From bbbbcbd369aafdd2314c7aec1883d350b35909ca Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:06:17 +0100 Subject: [PATCH] improve tests --- .github/workflows/e2e-tests.yml | 8 ++- .github/workflows/retag-commit.yml | 5 +- app/ci-start-app.js | 31 ++++++++++++ app/package.json | 4 +- app/playwright.config.ts | 2 +- app/playwright/tests/landingPageTests.spec.ts | 8 +-- app/playwright/tests/paidUserTests.spec.ts | 23 ++++++--- app/playwright/tests/unpaidUserTests.spec.ts | 27 +++++++--- app/playwright/tests/utils.ts | 50 ++++++++++--------- 9 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 app/ci-start-app.js diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3c4eaf32..b99a42ca 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout the repo uses: actions/checkout@v4 - + - name: Install Wasp run: curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s @@ -39,10 +39,8 @@ jobs: - name: Setup Env Vars not in github secrets run: | cd app - ls -la - test -f .env.server && echo ".env.server exists" || echo ".env.server does not exist" cp .env.server.example .env.server - + - name: Set up Playwright run: | cd app @@ -53,4 +51,4 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | cd app - DEBUG=pw:webserver npx playwright test \ No newline at end of file + DEBUG=pw:webserver npx playwright test diff --git a/.github/workflows/retag-commit.yml b/.github/workflows/retag-commit.yml index 5880c3a7..7c3887ea 100644 --- a/.github/workflows/retag-commit.yml +++ b/.github/workflows/retag-commit.yml @@ -4,7 +4,6 @@ on: push: branches: - main - jobs: retag: @@ -17,7 +16,7 @@ jobs: run: | git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" - + - name: Delete Old Tag run: | git tag -d wasp-v0.12-template || true @@ -26,4 +25,4 @@ jobs: - name: Add New Tag run: | git tag wasp-v0.12-template - git push origin wasp-v0.12-template \ No newline at end of file + git push origin wasp-v0.12-template diff --git a/app/ci-start-app.js b/app/ci-start-app.js new file mode 100644 index 00000000..4d253305 --- /dev/null +++ b/app/ci-start-app.js @@ -0,0 +1,31 @@ +import cp from 'child_process'; +import readline from 'linebyline'; + +function spawn(name, cmd, args, done) { + const spawnOptions = { + detached: true, + }; + const proc = cp.spawn(cmd, args, spawnOptions); + + // We close stdin stream on the new process because otherwise the start-app + // process hangs. + // See https://github.com/wasp-lang/wasp/pull/1218#issuecomment-1599098272. + proc.stdin.destroy(); + + readline(proc.stdout).on('line', (data) => { + console.log(`\x1b[0m\x1b[33m[${name}][out]\x1b[0m ${data}`); + }); + readline(proc.stderr).on('line', (data) => { + console.log(`\x1b[0m\x1b[33m[${name}][err]\x1b[0m ${data}`); + }); + proc.on('exit', done); +} + +// Exit if either child fails +const cb = (code) => { + if (code !== 0) { + process.exit(code); + } +}; +spawn('app', 'npm', ['run', 'example-app:start-app'], cb); +spawn('db', 'npm', ['run', 'example-app:start-db'], cb); diff --git a/app/package.json b/app/package.json index c9db3cb0..b0c6e6cd 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "opensaas", "scripts": { - "example-app:start": "npm-run-all --parallel \"example-app:start-db\" \"example-app:start-app\"", + "example-app:start": "node ci-start-app.js", "example-app:start-db": "npm run example-app:cleanup-db && wasp start db", "example-app:start-app": "npm run example-app:wait-for-db && wasp db migrate-dev && wasp start", "example-app:wait-for-db": "npx wait-port 5432", @@ -44,4 +44,4 @@ "typescript": "^5.1.0", "vite": "^4.3.9" } -} +} \ No newline at end of file diff --git a/app/playwright.config.ts b/app/playwright.config.ts index 99302ee3..7949a6e5 100644 --- a/app/playwright.config.ts +++ b/app/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', diff --git a/app/playwright/tests/landingPageTests.spec.ts b/app/playwright/tests/landingPageTests.spec.ts index 4ddabe10..b47ca505 100644 --- a/app/playwright/tests/landingPageTests.spec.ts +++ b/app/playwright/tests/landingPageTests.spec.ts @@ -1,8 +1,9 @@ import { test, expect } from '@playwright/test'; +import { DOCS_URL } from '../../src/shared/constants'; test.describe('general landing page tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('localhost:3000'); + await page.goto('/'); }); test('has title', async ({ page }) => { @@ -12,10 +13,11 @@ test.describe('general landing page tests', () => { test('get started link', async ({ page }) => { await page.getByRole('link', { name: 'Get started' }).click(); + await page.waitForURL(DOCS_URL); }); test('headings', async ({ page }) => { - expect(page.getByRole('heading', { name: 'SaaS' })).toBeTruthy(); - expect(page.getByRole('heading', { name: 'Features' })).toBeTruthy(); + await expect(page.getByRole('heading', { name: 'Frequently asked questions' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Some cool words' })).toBeVisible(); }); }); diff --git a/app/playwright/tests/paidUserTests.spec.ts b/app/playwright/tests/paidUserTests.spec.ts index c904b9c3..a34d2cdb 100644 --- a/app/playwright/tests/paidUserTests.spec.ts +++ b/app/playwright/tests/paidUserTests.spec.ts @@ -6,21 +6,24 @@ const test = createLoggedInUserFixture({ hasPaid: true, credits: 10 }); // test /demo-app page by entering "todo" and clicking add task test('Demo App: add tasks & generate schedule', async ({ loggedInPage }) => { - expect(loggedInPage.url()).toBe('http://localhost:3000/demo-app'); + const task1 = 'create presentation on SaaS'; + const task2 = 'build SaaS app draft'; + + await loggedInPage.waitForURL('/demo-app'); // Fill input id="description" with "create presentation" - await loggedInPage.fill('input[id="description"]', 'create presentation on SaaS'); + await loggedInPage.fill('input[id="description"]', task1); // Click button:has-text("Add task") await loggedInPage.click('button:has-text("Add task")'); - await loggedInPage.fill('input[id="description"]', 'build SaaS app draft'); + await loggedInPage.fill('input[id="description"]', task2); await loggedInPage.click('button:has-text("Add task")'); // expect to find text in a span element - expect(loggedInPage.getByText('create presentation on SaaS')).toBeTruthy(); - expect(loggedInPage.getByText('build SaaS app draft')).toBeTruthy(); + expect(loggedInPage.getByText(task1)).toBeTruthy(); + expect(loggedInPage.getByText(task2)).toBeTruthy(); // find a button with text "Generate Schedule" and check it's visible const generateScheduleButton = loggedInPage.getByRole('button', { name: 'Generate Schedule' }); @@ -31,7 +34,7 @@ test('Demo App: add tasks & generate schedule', async ({ loggedInPage }) => { (req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST' ), loggedInPage.waitForResponse((response) => { - if (response.url() === 'http://localhost:3001/operations/generate-gpt-response' && response.status() === 200) { + if (response.url().includes('/operations/generate-gpt-response') && response.status() === 200) { return true; } return false; @@ -39,4 +42,12 @@ test('Demo App: add tasks & generate schedule', async ({ loggedInPage }) => { // We already started waiting before we perform the click that triggers the API calls. So now we just perform the click generateScheduleButton.click(), ]); + + const table = loggedInPage.getByRole('table'); + await expect(table).toBeVisible(); + const tableTextContent = (await table.innerText()).toLowerCase(); + console.log(tableTextContent) + + expect(tableTextContent.includes(task1.toLowerCase())).toBeTruthy(); + expect(tableTextContent.includes(task2.toLowerCase())).toBeTruthy(); }); diff --git a/app/playwright/tests/unpaidUserTests.spec.ts b/app/playwright/tests/unpaidUserTests.spec.ts index c320e537..80f9fd7e 100644 --- a/app/playwright/tests/unpaidUserTests.spec.ts +++ b/app/playwright/tests/unpaidUserTests.spec.ts @@ -5,31 +5,36 @@ import { createLoggedInUserFixture } from './utils'; const test = createLoggedInUserFixture({ hasPaid: false, credits: 0 }); test('Demo app: cannot generate schedule', async ({ loggedInPage }) => { - await loggedInPage.waitForURL('http://localhost:3000/demo-app'); + const task1 = 'create presentation on SaaS'; + const task2 = 'build SaaS app draft'; + + await loggedInPage.waitForURL('/demo-app'); // Fill input id="description" with "create presentation" - await loggedInPage.fill('input[id="description"]', 'create presentation on SaaS'); + await loggedInPage.fill('input[id="description"]', task1); // Click button:has-text("Add task") await loggedInPage.click('button:has-text("Add task")'); - await loggedInPage.fill('input[id="description"]', 'build SaaS app draft'); + await loggedInPage.fill('input[id="description"]', task2); await loggedInPage.click('button:has-text("Add task")'); // expect to find text in a span element - expect(loggedInPage.getByText('create presentation on SaaS')).toBeTruthy(); - expect(loggedInPage.getByText('build SaaS app draft')).toBeTruthy(); + expect(loggedInPage.getByText(task1)).toBeTruthy(); + expect(loggedInPage.getByText(task2)).toBeTruthy(); // find a button with text "Generate Schedule" and check it's visible const generateScheduleButton = loggedInPage.getByRole('button', { name: 'Generate Schedule' }); expect(generateScheduleButton).toBeTruthy(); await Promise.all([ - loggedInPage.waitForRequest((req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'), + loggedInPage.waitForRequest( + (req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST' + ), loggedInPage.waitForResponse((response) => { // expect the response to be 402 "PAYMENT_REQUIRED" - if (response.url() === 'http://localhost:3001/operations/generate-gpt-response' && response.status() === 402) { + if (response.url().includes('/operations/generate-gpt-response') && response.status() === 402) { return true; } return false; @@ -37,4 +42,12 @@ test('Demo app: cannot generate schedule', async ({ loggedInPage }) => { // We already started waiting before we perform the click that triggers the API calls. So now we just perform the click generateScheduleButton.click(), ]); + + // we already show a table with some dummy data even before the API call + const table = loggedInPage.getByRole('table'); + await expect(table).toBeVisible(); + const tableTextContent = (await table.innerText()).toLowerCase(); + + expect(tableTextContent.includes(task1.toLowerCase())).toBeFalsy(); + expect(tableTextContent.includes(task2.toLowerCase())).toBeFalsy(); }); diff --git a/app/playwright/tests/utils.ts b/app/playwright/tests/utils.ts index 70d17aa9..d3039573 100644 --- a/app/playwright/tests/utils.ts +++ b/app/playwright/tests/utils.ts @@ -1,6 +1,7 @@ import { test as base, type Page } from '@playwright/test'; import { PrismaClient } from '@prisma/client'; -// Create a new Prisma client to interact with DB +import { randomUUID } from 'crypto'; + export const prisma = new PrismaClient(); export type User = { @@ -17,7 +18,7 @@ export const logUserIn = async ({ page, user }: { page: Page; user: User }) => { // Click the get started link. await page.getByRole('link', { name: 'Log in' }).click(); - console.log('logging in...', user) + console.log('logging in...', user); await page.waitForURL('http://localhost:3000/login'); console.log('url', page.url()); @@ -32,7 +33,7 @@ export const logUserIn = async ({ page, user }: { page: Page; user: User }) => { }; export const signUserUp = async ({ page, user }: { page: Page; user: User }) => { - await page.goto('localhost:3000'); + await page.goto('/'); // Click the get started link. await page.getByRole('link', { name: 'Log in' }).click(); @@ -51,28 +52,29 @@ export const signUserUp = async ({ page, user }: { page: Page; user: User }) => }; export const createRandomUser = () => { - const username = `user${Math.random().toString(36).substring(7)}`; - const password = `password${Math.random().toString(36).substring(7)}!`; + const username = `user${randomUUID()}`; + const password = `password${randomUUID()}!`; return { username, password }; }; -export const createLoggedInUserFixture = ({ hasPaid, credits }: Pick) => base.extend<{ loggedInPage: Page; testUser: User }>({ - testUser: async ({}, use) => { - const { username, password } = createRandomUser(); - await use({ username, password, hasPaid, credits }); - }, - loggedInPage: async ({ page, testUser }, use) => { - await signUserUp({ page, user: testUser }); - await page.waitForURL('http://localhost:3000/demo-app'); - const user = await prisma.user.update({ - where: { username: testUser.username }, - data: { hasPaid: testUser.hasPaid, credits: testUser.credits }, - }); - await use(page); - // clean up all that nasty data 🤮 - await prisma.gptResponse.deleteMany({ where: { userId: user.id } }); - await prisma.task.deleteMany({ where: { userId: user.id } }); - await prisma.user.delete({ where: { id: user.id } }); - }, -}); \ No newline at end of file +export const createLoggedInUserFixture = ({ hasPaid, credits }: Pick) => + base.extend<{ loggedInPage: Page; testUser: User }>({ + testUser: async ({}, use) => { + const { username, password } = createRandomUser(); + await use({ username, password, hasPaid, credits }); + }, + loggedInPage: async ({ page, testUser }, use) => { + await signUserUp({ page, user: testUser }); + await page.waitForURL('/demo-app'); + const user = await prisma.user.update({ + where: { username: testUser.username }, + data: { hasPaid: testUser.hasPaid, credits: testUser.credits }, + }); + await use(page); + // clean up all that nasty data 🤮 + await prisma.gptResponse.deleteMany({ where: { userId: user.id } }); + await prisma.task.deleteMany({ where: { userId: user.id } }); + await prisma.user.delete({ where: { id: user.id } }); + }, + });