Skip to content

Commit

Permalink
Merge pull request #2 from LucasSilbernagel/playwright
Browse files Browse the repository at this point in the history
Playwright
  • Loading branch information
LucasSilbernagel authored Nov 13, 2024
2 parents 651d4ed + dd90849 commit d7fba95
Show file tree
Hide file tree
Showing 7 changed files with 504 additions and 34 deletions.
79 changes: 79 additions & 0 deletions .github/workflows/playwright.yml
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules
/.cache
/build
.env
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
134 changes: 134 additions & 0 deletions e2e/decision-tree.spec.ts
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()
})
})
122 changes: 122 additions & 0 deletions e2e/helpers.ts
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()
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"test-unit": "vitest"
"test-unit": "vitest",
"test-e2e": " pnpm exec playwright test",
"test-e2e-ui": "pnpm exec playwright test --ui"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.2",
Expand All @@ -30,13 +32,15 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.12.1",
"@remix-run/testing": "^2.13.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/dompurify": "^3.0.5",
"@types/node": "^22.9.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
Expand Down
Loading

0 comments on commit d7fba95

Please sign in to comment.