@@ -356,4 +356,199 @@ The test should pass very quickly.
356
356
357
357
## Writing a hybrid UI/API test
358
358
359
- ?
359
+ Our second test will move a card from one project column to another.
360
+ In this test, we will use complementary API and UI interactions to cover this behavior.
361
+ Here are our steps:
362
+
363
+ 1 . Prep the test data
364
+ 2 . Create a new card (API)
365
+ 3 . Log into the GitHub website (UI)
366
+ 4 . Load the project page (UI)
367
+ 5 . Move the card from one column to another (UI)
368
+ 6 . Verify the card is in the second column (UI)
369
+ 7 . Verify the card change persisted to the backend (API)
370
+
371
+ Thankfully we can reuse many of the fixtures we created for the previous test.
372
+ Even though the previous test created a card, we must create a new card for this test.
373
+ Tests can run individually or out of order.
374
+ We should not create any interdependencies between individual test cases.
375
+
376
+ Let's dive directly into the test case.
377
+
378
+ We'll need to use Playwright's ` expect ` function for some of our new assertions.
379
+ Add this import statement to ` tests/test_github_project.py ` :
380
+
381
+ ``` python
382
+ from playwright.sync_api import expect
383
+ ```
384
+
385
+ Add a new test function with all these fixtures:
386
+
387
+ ``` python
388
+ def test_move_project_card (gh_context , gh_project , project_column_ids , page , gh_username , gh_password ):
389
+ ```
390
+
391
+ Moving a card requires two columns: the source column and the destination column.
392
+ For simplicity, let's use the first two columns,
393
+ and let's create convenient variables for their IDs:
394
+
395
+ ``` python
396
+ source_col = project_column_ids[0 ]
397
+ dest_col = project_column_ids[1 ]
398
+ ```
399
+
400
+ Just like in the previous test, we should write a unique note for the card to create:
401
+
402
+ ``` python
403
+ now = time.time()
404
+ note = f ' Move this card at { now} '
405
+ ```
406
+
407
+ The code to create a card via the GitHub API is pretty much the same as before, too:
408
+
409
+ ``` python
410
+ c_response = gh_context.post(
411
+ f ' /projects/columns/ { source_col} /cards ' ,
412
+ data = {' note' : note})
413
+ assert c_response.ok
414
+ ```
415
+
416
+ Now, it's time to switch from API to UI.
417
+ We need to log into the GitHub website to interact with this new card.
418
+ Log into GitHub like this, using fixtures for username and password:
419
+
420
+ ``` python
421
+ page.goto(f ' https://github.com/login ' )
422
+ page.fill(' id=login_field' , gh_username)
423
+ page.fill(' id=password' , gh_password)
424
+ page.click(' input[name="commit"]' )
425
+ ```
426
+
427
+ These interactions use ` Page ` methods we saw before in our DuckDuckGo search test.
428
+ Then, once logged in, navigate directly to the project page:
429
+
430
+ ``` python
431
+ page.goto(f ' https://github.com/users/ { gh_username} /projects/ { gh_project[" number" ]} ' )
432
+ ```
433
+
434
+ Direct URL navigation is faster and simpler than clicking through elements on pages.
435
+ We can retrieve the GitHub project number from the project's data.
436
+ (* Warning:* the project number for the URL is different from the project's ID number.)
437
+
438
+ For safety and sanity, we should check that the first project column has the card we created via API:
439
+
440
+ ``` python
441
+ card_xpath = f ' //div[@id="column-cards- { source_col} "]//p[contains(text(), " { note} ")] '
442
+ expect(page.locator(card_xpath)).to_be_visible()
443
+ ```
444
+
445
+ The card XPath is complicated.
446
+ Let's break it down:
447
+
448
+ 1 . ` //div[@id="column-cards-{source_col}"] ` locates the source column ` div ` using its ID
449
+ 2 . ` //p[contains(text(), "{note}")] ` locates a child ` p ` that contains the text of the target card's note
450
+
451
+ The assertion is also a bit complex.
452
+ Let's break it down, too:
453
+
454
+ 1 . ` expect(...) ` is a special Playwright function for assertions on page locators.
455
+ 2 . ` page.locator(card_xpath) ` is a web element locator for the target card.
456
+ 3 . ` to_be_visible() ` is a condition method for the ` expect ` assertion.
457
+ It verifies that the "expected" locator's element is visible on the page.
458
+
459
+ Since the locator includes the source column as the parent for the card's paragraph,
460
+ asserting its visibility on the page is sufficient for verifying correctness.
461
+ If we only checked for the paragraph element without the parent column,
462
+ then the test would not detect if the card appeared in the wrong column.
463
+ Furthermore, Playwright assertions will automatically wait up to a timeout for conditions to become true.
464
+
465
+ Now, we can perform the main interaction:
466
+ moving the card from one column to another.
467
+ Playwright provides a nifty
468
+ [ ` drag_and_drop ` ] ( https://playwright.dev/python/docs/api/class-page#page-drag-and-drop ) method:
469
+
470
+ ``` python
471
+ page.drag_and_drop(f ' text=" { note} " ' , f ' id=column-cards- { dest_col} ' )
472
+ ```
473
+
474
+ This call will drag the card to the destination column.
475
+ Here, we can use a simpler locator for the card because we previously verified its correct placement.
476
+
477
+ After moving the card, we should verify that it indeed appears in the destination column:
478
+
479
+ ``` python
480
+ card_xpath = f ' //div[@id="column-cards- { dest_col} "]//p[contains(text(), " { note} ")] '
481
+ expect(page.locator(card_xpath)).to_be_visible()
482
+ ```
483
+
484
+ Finally, we should also check that the card's changes persisted to the backend.
485
+ Let's ` GET ` that card's most recent data via the API:
486
+
487
+ ``` python
488
+ card_id = c_response.json()[' id' ]
489
+ r_response = gh_context.get(f ' /projects/columns/cards/ { card_id} ' )
490
+ assert r_response.ok
491
+ assert r_response.json()[' column_url' ].endswith(str (dest_col))
492
+ ```
493
+
494
+ The way to verify the column update is to check the new ID in the ` column_url ` value.
495
+
496
+ Here's the completed test code for ` test_move_project_card ` :
497
+
498
+ ``` python
499
+ import time
500
+ from playwright.sync_api import expect
501
+
502
+ def test_move_project_card (gh_context , gh_project , project_column_ids , page , gh_username , gh_password ):
503
+
504
+ # Prep test data
505
+ source_col = project_column_ids[0 ]
506
+ dest_col = project_column_ids[1 ]
507
+ now = time.time()
508
+ note = f ' Move this card at { now} '
509
+
510
+ # Create a new card via API
511
+ c_response = gh_context.post(
512
+ f ' /projects/columns/ { source_col} /cards ' ,
513
+ data = {' note' : note})
514
+ assert c_response.ok
515
+
516
+ # Log in via UI
517
+ page.goto(f ' https://github.com/login ' )
518
+ page.fill(' id=login_field' , gh_username)
519
+ page.fill(' id=password' , gh_password)
520
+ page.click(' input[name="commit"]' )
521
+
522
+ # Load the project page
523
+ page.goto(f ' https://github.com/users/ { gh_username} /projects/ { gh_project[" number" ]} ' )
524
+
525
+ # Verify the card appears in the first column
526
+ card_xpath = f ' //div[@id="column-cards- { source_col} "]//p[contains(text(), " { note} ")] '
527
+ expect(page.locator(card_xpath)).to_be_visible()
528
+
529
+ # Move a card to the second column via web UI
530
+ page.drag_and_drop(f ' text=" { note} " ' , f ' id=column-cards- { dest_col} ' )
531
+
532
+ # Verify the card is in the second column via UI
533
+ card_xpath = f ' //div[@id="column-cards- { dest_col} "]//p[contains(text(), " { note} ")] '
534
+ expect(page.locator(card_xpath)).to_be_visible()
535
+
536
+ # Verify the backend is updated via API
537
+ card_id = c_response.json()[' id' ]
538
+ r_response = gh_context.get(f ' /projects/columns/cards/ { card_id} ' )
539
+ assert r_response.ok
540
+ assert r_response.json()[' column_url' ].endswith(str (dest_col))
541
+ ```
542
+
543
+ Run this new test.
544
+ If you want to see the browser in action, included the ` --headed ` option.
545
+ The test will take a few seconds longer than the pure API test,
546
+ but both should pass!
547
+
548
+ > * Warning:*
549
+ > You might want to periodically archive cards in your GitHub project
550
+ > that are created by these tests.
551
+
552
+ Complementing UI interactions with API calls is a great way to optimize test execution.
553
+ Instead of doing all test steps through the UI, which is slower and more prone to race conditions,
554
+ certain actions like pre-loading data or verifying persistent changes can be handled with API calls.
0 commit comments