-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from LucasSilbernagel/playwright
Playwright
- Loading branch information
Showing
7 changed files
with
504 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
name: Playwright Tests | ||
on: | ||
pull_request: | ||
branches: [main] | ||
|
||
jobs: | ||
test: | ||
timeout-minutes: 60 | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Install pnpm | ||
uses: pnpm/action-setup@v2 | ||
with: | ||
version: latest | ||
|
||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: lts/* | ||
cache: 'pnpm' | ||
|
||
- name: Install dependencies | ||
run: pnpm install | ||
|
||
- name: Install Playwright Browsers | ||
run: pnpm exec playwright install --with-deps | ||
|
||
- name: Wait for Vercel Preview | ||
id: wait_for_preview | ||
run: | | ||
# Maximum wait time in seconds (5 minutes) | ||
max_wait=300 | ||
wait_interval=10 | ||
elapsed=0 | ||
while [ $elapsed -lt $max_wait ]; do | ||
PR_NUMBER=${{ github.event.pull_request.number }} | ||
DEPLOYMENTS=$(curl -s -H "Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}" \ | ||
"https://api.vercel.com/v6/deployments?meta-githubPrId=$PR_NUMBER&state=READY&limit=1") | ||
PREVIEW_URL=$(echo $DEPLOYMENTS | jq -r '.deployments[0].url') | ||
if [ ! -z "$PREVIEW_URL" ] && [ "$PREVIEW_URL" != "null" ]; then | ||
if [[ $PREVIEW_URL != http* ]]; then | ||
PREVIEW_URL="https://$PREVIEW_URL" | ||
fi | ||
echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_OUTPUT | ||
echo "Preview URL found: $PREVIEW_URL" | ||
exit 0 | ||
fi | ||
echo "Waiting for Vercel preview deployment... (${elapsed}s elapsed)" | ||
sleep $wait_interval | ||
elapsed=$((elapsed + wait_interval)) | ||
done | ||
echo "Timeout waiting for Vercel preview deployment" | ||
exit 1 | ||
env: | ||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | ||
|
||
- name: Run Playwright tests | ||
run: | | ||
echo "Testing against URL: $PLAYWRIGHT_TEST_BASE_URL" | ||
env: | ||
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.wait_for_preview.outputs.PREVIEW_URL }} | ||
|
||
- name: Upload test results | ||
if: always() | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: playwright-report | ||
path: | | ||
playwright-report/ | ||
test-results/ | ||
retention-days: 30 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,7 @@ node_modules | |
/.cache | ||
/build | ||
.env | ||
/test-results/ | ||
/playwright-report/ | ||
/blob-report/ | ||
/playwright/.cache/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { test, expect } from '@playwright/test' | ||
import { | ||
checkHomepageContents, | ||
loadExampleTree, | ||
loadHomepage, | ||
loadNewTree, | ||
} from './helpers' | ||
|
||
test.describe('pageload', () => { | ||
test('should load landing page content as expected', async ({ page }) => { | ||
await loadHomepage(page) | ||
}) | ||
|
||
test('should load 404 page content as expected', async ({ page }) => { | ||
await page.goto('/random-invalid-page') | ||
await page.waitForLoadState('networkidle') | ||
await expect(page).toHaveTitle(/404/i) | ||
await expect(page.getByText('404')).toBeVisible() | ||
await expect(page.getByText('Page not found.')).toBeVisible() | ||
await expect(page.getByRole('link', { name: /Home/i })).toBeVisible() | ||
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible() | ||
await page.click('text=Home') | ||
await page.waitForLoadState('networkidle') | ||
await expect(page).toHaveTitle(/Home/i) | ||
await page.goto('/random-invalid-page') | ||
await page.waitForLoadState('networkidle') | ||
await expect(page).toHaveTitle(/404/i) | ||
}) | ||
}) | ||
|
||
test.describe('decision tree functionality', () => { | ||
test('should create a new decision tree on button click', async ({ | ||
page, | ||
}) => { | ||
await loadNewTree(page) | ||
}) | ||
|
||
test('should load an example decision tree on button click', async ({ | ||
page, | ||
}) => { | ||
await loadExampleTree(page) | ||
}) | ||
|
||
test('should copy decision tree URL on button click', async ({ page }) => { | ||
await loadNewTree(page) | ||
await page.evaluate(() => { | ||
navigator.clipboard.writeText = async () => Promise.resolve() | ||
}) | ||
await page.click('role=button[name="Share Tree"]') | ||
await expect( | ||
page.locator('div.text-sm.font-semibold', { | ||
hasText: 'URL copied to clipboard!', | ||
}) | ||
).toBeVisible() | ||
}) | ||
|
||
test('should clear the decision tree and return to the landing page on button click', async ({ | ||
page, | ||
}) => { | ||
await loadExampleTree(page) | ||
await page.click('role=button[name="Start Over"]') | ||
await expect( | ||
page.getByRole('heading', { name: 'Start Over' }) | ||
).toBeVisible() | ||
await expect(page.getByText('Are you sure you want to')).toBeVisible() | ||
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible() | ||
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible() | ||
await page.click('role=button[name="Cancel"]') | ||
await expect( | ||
page.getByRole('heading', { name: 'Start Over' }) | ||
).not.toBeVisible() | ||
await page.click('role=button[name="Start Over"]') | ||
await page.click('role=button[name="Continue"]') | ||
await page.waitForLoadState('networkidle') | ||
await expect(page).toHaveTitle(/Home/i) | ||
await checkHomepageContents(page) | ||
}) | ||
|
||
test('should be able to edit the decision tree title', async ({ page }) => { | ||
await loadNewTree(page) | ||
await page.click('role=button[name="Decision Tree Title - edit"]') | ||
await expect( | ||
page.getByRole('button', { name: /Decision Tree Title/i }) | ||
).not.toBeVisible() | ||
await expect(page.getByPlaceholder('Decision Tree Title')).toBeVisible() | ||
await page.fill('[placeholder="Decision Tree Title"]', 'My fancy new title') | ||
await page.keyboard.press('Enter') | ||
await expect(page.getByPlaceholder('Decision Tree Title')).not.toBeVisible() | ||
await expect(page.getByLabel('My fancy new title - edit')).toBeVisible() | ||
}) | ||
|
||
test('should be able to edit decision node text', async ({ page }) => { | ||
await loadNewTree(page) | ||
await page | ||
.getByLabel('Decision root: Yes or no?.') | ||
.getByLabel('edit text') | ||
.click() | ||
await expect(page.getByPlaceholder('Yes or no?')).toBeVisible() | ||
await page.fill('[placeholder="Yes or no?"]', 'Example decision node text.') | ||
await page.keyboard.press('Enter') | ||
await expect(page.getByPlaceholder('Yes or no?')).not.toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision root: Example decision node text.') | ||
).toBeVisible() | ||
}) | ||
|
||
test('should be able to add a new decision node', async ({ page }) => { | ||
await loadExampleTree(page) | ||
await page | ||
.getByLabel('Decision : Pick up a salad or') | ||
.getByLabel('Add child nodes') | ||
.click() | ||
await expect( | ||
page.getByLabel('Decision : No.').getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : Yes.').getByLabel('edit text') | ||
).toBeVisible() | ||
}) | ||
|
||
test('should be able to delete a decision node', async ({ page }) => { | ||
await loadExampleTree(page) | ||
await expect( | ||
page.getByLabel('Decision : Pick up a salad or').getByLabel('edit text') | ||
).toBeVisible() | ||
await page | ||
.getByLabel('Decision : Pick up a salad or') | ||
.getByLabel('Delete node') | ||
.click() | ||
await expect( | ||
page.getByLabel('Decision : Pick up a salad or').getByLabel('edit text') | ||
).not.toBeVisible() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Page, expect } from '@playwright/test' | ||
|
||
export const checkHomepageContents = async (page: Page) => { | ||
await expect(page.getByRole('button', { name: /Example/i })).toBeVisible() | ||
await expect(page.getByRole('button', { name: /New/i })).toBeVisible() | ||
await expect( | ||
page.getByText( | ||
'A simple generator of shareable and accessible decision trees.' | ||
) | ||
).toBeVisible() | ||
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible() | ||
} | ||
|
||
export const loadHomepage = async (page: Page) => { | ||
await page.goto('/') | ||
await page.waitForLoadState('networkidle') | ||
await expect(page).toHaveTitle(/Home/i) | ||
await checkHomepageContents(page) | ||
} | ||
|
||
export const loadNewTree = async (page: Page) => { | ||
await loadHomepage(page) | ||
await page.click('text=New') | ||
await expect(page.getByRole('button', { name: /Share Tree/i })).toBeVisible() | ||
await expect(page.getByRole('button', { name: /Start Over/i })).toBeVisible() | ||
await expect( | ||
page.getByRole('button', { name: /Decision Tree Title/i }) | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision root: Yes or no?.').getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : No.').getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : No.').getByLabel('Add child nodes') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : Yes.').getByLabel('Add child nodes') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : Yes.').getByLabel('edit text') | ||
).toBeVisible() | ||
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible() | ||
} | ||
|
||
export const loadExampleTree = async (page: Page) => { | ||
await loadHomepage(page) | ||
await page.click('text=Example') | ||
await expect(page.getByRole('button', { name: /Share Tree/i })).toBeVisible() | ||
await expect(page.getByRole('button', { name: /Start Over/i })).toBeVisible() | ||
await expect( | ||
page.getByRole('button', { name: /What should you have for lunch?/i }) | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.getByLabel('Decision : Do you want to go to a sit-down restaurant?') | ||
.getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.getByLabel('Decision : Do you have ingredients to make a sandwich?') | ||
.getByLabel('edit text') | ||
).toBeVisible() | ||
await expect( | ||
page.getByLabel('Decision : Pick up a salad or').getByLabel('Delete node') | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.getByLabel('Decision : Pick up a salad or') | ||
.getByLabel('Add child nodes') | ||
).toBeVisible() | ||
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible() | ||
await expect( | ||
page.getByRole('button', { name: /Back to start/i }) | ||
).not.toBeVisible() | ||
const scrollableArea = page.getByLabel('Decision tree navigation area') | ||
await scrollableArea.waitFor({ state: 'attached' }) | ||
let scrollSuccess = false | ||
for (let i = 0; i < 3; i++) { | ||
try { | ||
// Simple scroll first | ||
await scrollableArea.evaluate((div) => { | ||
div.scrollTop = div.scrollHeight | ||
}) | ||
// Wait a bit for scroll to settle | ||
await page.waitForTimeout(100) | ||
// Verify scroll position | ||
const isScrolled = await scrollableArea.evaluate((div) => { | ||
const maxScroll = div.scrollHeight - div.clientHeight | ||
return Math.abs(div.scrollTop - maxScroll) < 1 | ||
}) | ||
if (isScrolled) { | ||
scrollSuccess = true | ||
break | ||
} | ||
} catch (error) { | ||
if (i === 2) throw error // On last attempt, throw the error | ||
await page.waitForTimeout(100) // Wait before retry | ||
} | ||
} | ||
if (!scrollSuccess) { | ||
throw new Error('Failed to scroll to bottom') | ||
} | ||
// Wait for button to be visible with retry | ||
await expect( | ||
page.getByRole('button', { name: /Back to start/i }) | ||
).toBeVisible({ timeout: 5000 }) | ||
await expect( | ||
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text') | ||
).not.toBeInViewport() | ||
await page.getByRole('button', { name: /Back to start/i }).click({ | ||
timeout: 5000, | ||
force: true, // Use force: true to click even if element is moving | ||
}) | ||
await expect( | ||
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text') | ||
).toBeInViewport() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.