Skip to content

Commit 31be982

Browse files
Improved local variable names
1 parent 743cb00 commit 31be982

File tree

1 file changed

+196
-1
lines changed

1 file changed

+196
-1
lines changed

tutorial/6-api-testing.md

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,199 @@ The test should pass very quickly.
356356

357357
## Writing a hybrid UI/API test
358358

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

Comments
 (0)