From fd9d6b70812c04ddd3c22fcb8e3156b6ae9c0f19 Mon Sep 17 00:00:00 2001 From: Ty Date: Thu, 13 Feb 2025 11:30:18 -0500 Subject: [PATCH 01/10] Add: basic e2e tests + readme instructions. --- README.md | 8 +++ mix.exs | 3 +- test/e2e/.gitignore | 7 +++ test/e2e/login.spec.ts | 77 +++++++++++++++++++++++++++ test/e2e/package-lock.json | 97 +++++++++++++++++++++++++++++++++++ test/e2e/package.json | 18 +++++++ test/e2e/playwright.config.ts | 79 ++++++++++++++++++++++++++++ 7 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 test/e2e/.gitignore create mode 100644 test/e2e/login.spec.ts create mode 100644 test/e2e/package-lock.json create mode 100644 test/e2e/package.json create mode 100644 test/e2e/playwright.config.ts diff --git a/README.md b/README.md index fc489c1c..6b473347 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,11 @@ Copyright 2021 The Bike Brigade Inc. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + ## End to End testing with Playwright + + To run e2e tests: + + 1. navigate to `/test/e2e` and run `npm install` + 1. from the root directory, run `mix test.e2e` - (THIS WILL WIPE YOUR LOCAL DB AND RESET IT) + 1. in a neew terminal, navigate to `/test/e2e` and run `npm run test:ui` diff --git a/mix.exs b/mix.exs index 82be0a53..cc7b1310 100644 --- a/mix.exs +++ b/mix.exs @@ -123,7 +123,8 @@ defmodule BikeBrigade.MixProject do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"], - "assets.deploy": ["esbuild default --minify", "tailwind default --minify", "phx.digest"] + "assets.deploy": ["esbuild default --minify", "tailwind default --minify", "phx.digest"], + "test.e2e": ["ecto.reset", "phx.server"] ] end diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore new file mode 100644 index 00000000..58786aac --- /dev/null +++ b/test/e2e/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts new file mode 100644 index 00000000..add4d945 --- /dev/null +++ b/test/e2e/login.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; + + +async function doLogin(page: any) { + await page.goto('http://localhost:4000/login'); + await page.getByRole('textbox', { name: 'Phone Number' }).click(); + await page.getByRole('textbox', { name: 'Phone Number' }).fill('6475555555'); + await page.getByRole('button', { name: 'Get Login Code' }).click(); + await page.getByRole('textbox', { name: 'Authentication Code' }).click(); + await page.getByRole('textbox', { name: 'Authentication Code' }).fill('123456'); + await page.getByRole('button', { name: 'Sign in' }).click(); +} + +test('standard flow', async ({ page }) => { + await doLogin(page) + await createProgram(page); + await editProgram(page) + await createCampaign(page); + +}); + +// requires login +async function createProgram(page: any) { + await page.goto('http://localhost:4000/programs'); + + const browserName = page.context().browser()?.browserType().name(); + const programName = `Program ${Date.now()}`; + + await page.getByRole('link', { name: 'Programs' }).click(); + await page.getByRole('link', { name: 'New Program' }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).fill(programName); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program'); + await page.getByRole('textbox', { name: 'About (internal description)' }).click(); + await page.getByRole('textbox', { name: 'About (internal description)' }).fill('This is an internal description'); + await page.getByRole('textbox', { name: 'Start Date' }).fill('2025-02-12'); + await page.getByRole('checkbox', { name: 'Public' }).check(); + await page.getByRole('checkbox', { name: 'Hide Pickup Address' }).check(); + await page.getByRole('button', { name: 'Add Schedule' }).click(); + await page.getByRole('textbox', { name: 'Photo Descriotion' }).click(); + await page.getByRole('textbox', { name: 'Photo Descriotion' }).fill('1 Large Box'); + await page.getByRole('textbox', { name: 'Contact Name' }).click(); + await page.getByRole('textbox', { name: 'Contact Name' }).fill('Joe Cool'); + await page.getByRole('textbox', { name: 'Contact Name' }).press('Tab'); + await page.getByRole('textbox', { name: 'Contact Email' }).fill('joecool@gmail.com'); + await page.getByRole('textbox', { name: 'Contact Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Contact Phone' }).fill('6475555554'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible(); +} + +async function editProgram(page: any) { + await page.goto('http://localhost:4000/programs'); + await page.getByRole('link', { name: 'Edit , Program' }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Program Updated'); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByRole('link', { name: 'Program Updated', exact: true })).toBeVisible(); + await expect(page.getByText('Success! program updated')).toBeVisible(); +} + +async function createCampaign(page: any) { + await page.getByRole('link', { name: 'Campaigns' }).click(); + await page.getByRole('link', { name: 'New Campaign' }).click(); + // TODO: change date for campaign to take param from function + await page.getByRole('textbox', { name: 'Delivery Date' }).fill('2025-02-20'); + await page.locator('#user-form_program_id').selectOption('3'); + await page.locator('#location-form-location-input-open').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); + await page.waitForTimeout(2000); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Success! Campaign created')).toBeVisible({timeout: 10000}); +} diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json new file mode 100644 index 00000000..01a421a0 --- /dev/null +++ b/test/e2e/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.50.1", + "@types/node": "^22.13.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", + "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/e2e/package.json b/test/e2e/package.json new file mode 100644 index 00000000..960464b6 --- /dev/null +++ b/test/e2e/package.json @@ -0,0 +1,18 @@ +{ + "name": "e2e", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "npx playwright test", + "test:ui": "npx playwright test --ui", + "test:withreport": "npm run test; npx playwright show-report" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.50.1", + "@types/node": "^22.13.1" + } +} \ No newline at end of file diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts new file mode 100644 index 00000000..e71befde --- /dev/null +++ b/test/e2e/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './.', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* 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', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); From c52c38b86eadeaaa7273ecd18a41e0b2a62e6d44 Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 17 Feb 2025 12:02:57 -0500 Subject: [PATCH 02/10] [wip] add login spec --- test/e2e/login.spec.ts | 68 +++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts index add4d945..2c6160a4 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/login.spec.ts @@ -1,5 +1,16 @@ import { test, expect } from '@playwright/test'; +test('standard flow', async ({ page }) => { + + const programName = `Test Program ${Date.now()}`; + + await doLogin(page) + await createProgram(page, programName); + await editProgram(page, programName) + await createCampaign(page); + await createCampaignForNextWeek(page) +}); + async function doLogin(page: any) { await page.goto('http://localhost:4000/login'); @@ -11,20 +22,12 @@ async function doLogin(page: any) { await page.getByRole('button', { name: 'Sign in' }).click(); } -test('standard flow', async ({ page }) => { - await doLogin(page) - await createProgram(page); - await editProgram(page) - await createCampaign(page); - -}); // requires login -async function createProgram(page: any) { +async function createProgram(page: any, programName: string) { await page.goto('http://localhost:4000/programs'); - const browserName = page.context().browser()?.browserType().name(); - const programName = `Program ${Date.now()}`; + // const browserName = page.context().browser()?.browserType().name(); await page.getByRole('link', { name: 'Programs' }).click(); await page.getByRole('link', { name: 'New Program' }).click(); @@ -50,24 +53,27 @@ async function createProgram(page: any) { await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible(); } -async function editProgram(page: any) { +// TODO: leaving off; don't change the name of the program when testing it was updated. +async function editProgram(page: any, programName: string) { + + const programNameUpdated = `Test Program ${Date.now()}`; await page.goto('http://localhost:4000/programs'); - await page.getByRole('link', { name: 'Edit , Program' }).click(); + + await page.getByRole('link', { name: `Edit , ${programName}` }).click(); await page.getByRole('textbox', { name: 'Name', exact: true }).click(); - await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Program Updated'); + await page.getByRole('textbox', { name: 'Name', exact: true }).fill(programNameUpdated); await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated'); await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByRole('link', { name: 'Program Updated', exact: true })).toBeVisible(); + await expect(page.getByRole('link', { name: programNameUpdated, exact: true })).toBeVisible(); await expect(page.getByText('Success! program updated')).toBeVisible(); } async function createCampaign(page: any) { await page.getByRole('link', { name: 'Campaigns' }).click(); await page.getByRole('link', { name: 'New Campaign' }).click(); - // TODO: change date for campaign to take param from function - await page.getByRole('textbox', { name: 'Delivery Date' }).fill('2025-02-20'); - await page.locator('#user-form_program_id').selectOption('3'); + await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); + await page.locator('#user-form_program_id').selectOption('3'); await page.locator('#location-form-location-input-open').click(); await page.locator('[id="campaign_form\\[location\\]_address"]').click(); await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); @@ -75,3 +81,31 @@ async function createCampaign(page: any) { await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText('Success! Campaign created')).toBeVisible({timeout: 10000}); } + +async function createCampaignForNextWeek(page: any) { + await page.getByRole('link', { name: 'Campaigns' }).click(); + await page.getByRole('link', { name: 'New Campaign' }).click(); + await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); + // out of convenience; we select the third; ie, the one we greated in createProgram + await page.locator('#user-form_program_id').selectOption('3'); + await page.locator('#location-form-location-input-open').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); + await page.waitForTimeout(2000); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Success! Campaign created')).toBeVisible({timeout: 10000}); + // TODO: test that you can see the campaign next week. +} + + +function getDatePlusDays(daysToAdd: number) { + const today = new Date(); + const futureDate = new Date(today); + futureDate.setDate(today.getDate() + daysToAdd); + + const year = futureDate.getFullYear(); + const month = String(futureDate.getMonth() + 1).padStart(2, '0'); + const day = String(futureDate.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} From d56251760f42f3975304d38f0379475518205b4e Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 17 Feb 2025 13:10:25 -0500 Subject: [PATCH 03/10] break tests into proper blocks --- test/e2e/login.spec.ts | 201 ++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 71 deletions(-) diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts index 2c6160a4..14007b06 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/login.spec.ts @@ -1,16 +1,109 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, Page } from '@playwright/test'; -test('standard flow', async ({ page }) => { +const programName = `Test Program ${Date.now()}`; - const programName = `Test Program ${Date.now()}`; +test.describe('Login and Logout', () => { + test('Can Login', async ({ page }) => { + await doLogin(page) + await expect(page.locator('#flash')).toContainText('Success! Welcome!'); + }) + test('Validates phone number', async ({ page }) => { + await page.goto('http://localhost:4000/login'); + await page.getByRole('textbox', { name: 'Phone Number' }).click(); + await page.getByRole('textbox', { name: 'Phone Number' }).fill('647555'); + await page.getByRole('button', { name: 'Get Login Code' }).click(); + await expect(page.locator('#login-form')).toContainText('phone number is not valid for Canada'); + }); - await doLogin(page) - await createProgram(page, programName); - await editProgram(page, programName) - await createCampaign(page); - await createCampaignForNextWeek(page) -}); + test('Cancel button returns to login page', async ({ page }) => { + await page.goto('http://localhost:4000/login'); + await page.getByRole('textbox', { name: 'Phone Number' }).click(); + await page.getByRole('textbox', { name: 'Phone Number' }).fill('6475555555'); + await page.getByRole('button', { name: 'Get Login Code' }).click(); + await page.getByRole('link', { name: 'Cancel' }).click(); + await expect(page.getByRole('button')).toContainText('Get Login Code'); + }); + test('Clicking sign up takes you to marketing site', async ({ page }) => { + await page.goto('http://localhost:4000/login'); + await page.getByRole('link', { name: 'Sign Up!' }).click(); + await expect(page.locator('h2')).toContainText('Join the Brigade.'); + }); + test('Can Logout', async ({ page }) => { + await doLogin(page) + await page.getByRole('link', { name: 'Log out' }).click(); + await expect(page.locator('#flash')).toContainText('Success! Goodbye'); + }); +}) + +test.describe('Programs', () => { + + test('Can create and edit program', async ({ page }) => { + await doLogin(page) // only needs to run once per describe block + await page.goto('http://localhost:4000/programs'); + + createProgram(page, programName) + await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible(); + + // editing of program + + await page.getByRole('link', { name: `Edit , ${programName}` }).click(); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Success! program updated')).toBeVisible(); + + await page.getByRole('link', { name: `Edit , ${programName}` }).click(); + await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); + await expect(page.getByLabel('Campaign Blurb (please keep')).toContainText('This is a test program that was updated'); + + }) +}) + +test.describe('Campaigns', () => { + let page: Page + + test.beforeAll(async ({ browser }) => { + const programName = `Test Program for New Campaign ${Date.now()}`; + page = await browser.newPage() + await doLogin(page) + await createProgram(page, programName) + }) + + test('Can create a campaign', async () => { + await page.goto('http://localhost:4000/campaigns/new'); + await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); + // TODO: this won't select option by label, meaning it's selecting an arbitrary program. + await page.locator('#user-form_program_id').selectOption("3"); // << this works but isn't ideal + await page.locator('#location-form-location-input-open').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); + await page.waitForTimeout(2000); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); + }) + + test('Can create a campaign for next week', async () => { + await page.goto('http://localhost:4000/campaigns/new'); + await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); + // TODO: this won't select option by label, meaning it's selecting an arbitrary program. + await page.locator('#user-form_program_id').selectOption({label: programName}); + // await page.locator('#user-form_program_id').selectOption("3"); + await page.locator('#location-form-location-input-open').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').click(); + await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); + await page.waitForTimeout(2000); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); + // now go check that campaign shows up next week. + await page.getByRole('link', { name: 'Campaigns' }).click(); + // goto next week + await page.getByRole('navigation', { name: 'Pagination' }).getByRole('link').nth(2).click(); + // ensure that new campaign for next-week is present + await expect(page.locator('#campaign-4')).toContainText('Test Program 1739811044662 edited'); + await expect(page.getByRole('link', { name: 'Test Program 1739811044662' })).toBeVisible(); + }) +}) async function doLogin(page: any) { await page.goto('http://localhost:4000/login'); @@ -23,12 +116,36 @@ async function doLogin(page: any) { } -// requires login -async function createProgram(page: any, programName: string) { - await page.goto('http://localhost:4000/programs'); +// async function createCampaign(page: any) { +// await page.getByRole('link', { name: 'Campaigns' }).click(); +// await page.getByRole('link', { name: 'New Campaign' }).click(); +// await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); +// await page.locator('#user-form_program_id').selectOption({}); +// await page.locator('#location-form-location-input-open').click(); +// await page.locator('[id="campaign_form\\[location\\]_address"]').click(); +// await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); +// await page.waitForTimeout(2000); +// await page.getByRole('button', { name: 'Save' }).click(); +// await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); +// } + +// async function createCampaignForNextWeek(page: any) { } + - // const browserName = page.context().browser()?.browserType().name(); +function getDatePlusDays(daysToAdd: number) { + const today = new Date(); + const futureDate = new Date(today); + futureDate.setDate(today.getDate() + daysToAdd); + + const year = futureDate.getFullYear(); + const month = String(futureDate.getMonth() + 1).padStart(2, '0'); + const day = String(futureDate.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} +async function createProgram(page: Page, programName: string) { + await page.goto('http://localhost:4000/programs'); await page.getByRole('link', { name: 'Programs' }).click(); await page.getByRole('link', { name: 'New Program' }).click(); await page.getByRole('textbox', { name: 'Name', exact: true }).click(); @@ -50,62 +167,4 @@ async function createProgram(page: any, programName: string) { await page.getByRole('textbox', { name: 'Contact Email' }).press('Tab'); await page.getByRole('textbox', { name: 'Contact Phone' }).fill('6475555554'); await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible(); -} - -// TODO: leaving off; don't change the name of the program when testing it was updated. -async function editProgram(page: any, programName: string) { - - const programNameUpdated = `Test Program ${Date.now()}`; - await page.goto('http://localhost:4000/programs'); - - await page.getByRole('link', { name: `Edit , ${programName}` }).click(); - await page.getByRole('textbox', { name: 'Name', exact: true }).click(); - await page.getByRole('textbox', { name: 'Name', exact: true }).fill(programNameUpdated); - await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); - await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated'); - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByRole('link', { name: programNameUpdated, exact: true })).toBeVisible(); - await expect(page.getByText('Success! program updated')).toBeVisible(); -} - -async function createCampaign(page: any) { - await page.getByRole('link', { name: 'Campaigns' }).click(); - await page.getByRole('link', { name: 'New Campaign' }).click(); - await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); - await page.locator('#user-form_program_id').selectOption('3'); - await page.locator('#location-form-location-input-open').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); - await page.waitForTimeout(2000); - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText('Success! Campaign created')).toBeVisible({timeout: 10000}); -} - -async function createCampaignForNextWeek(page: any) { - await page.getByRole('link', { name: 'Campaigns' }).click(); - await page.getByRole('link', { name: 'New Campaign' }).click(); - await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); - // out of convenience; we select the third; ie, the one we greated in createProgram - await page.locator('#user-form_program_id').selectOption('3'); - await page.locator('#location-form-location-input-open').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); - await page.waitForTimeout(2000); - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText('Success! Campaign created')).toBeVisible({timeout: 10000}); - // TODO: test that you can see the campaign next week. -} - - -function getDatePlusDays(daysToAdd: number) { - const today = new Date(); - const futureDate = new Date(today); - futureDate.setDate(today.getDate() + daysToAdd); - - const year = futureDate.getFullYear(); - const month = String(futureDate.getMonth() + 1).padStart(2, '0'); - const day = String(futureDate.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; } From 59de3d7693a224327986bb9efa3df974b5d87a91 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 19 Feb 2025 19:53:51 -0500 Subject: [PATCH 04/10] Add: test for new campaigns. --- test/e2e/login.spec.ts | 56 ++++++++++++++++---------------------- test/e2e/package-lock.json | 18 ++++++++++++ test/e2e/package.json | 3 +- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts index 14007b06..a32aaa8a 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/login.spec.ts @@ -1,6 +1,7 @@ import { test, expect, Page } from '@playwright/test'; +import { faker } from '@faker-js/faker'; -const programName = `Test Program ${Date.now()}`; +const programName = faker.company.name() test.describe('Login and Logout', () => { test('Can Login', async ({ page }) => { @@ -62,46 +63,51 @@ test.describe('Programs', () => { test.describe('Campaigns', () => { let page: Page + let programName: string test.beforeAll(async ({ browser }) => { - const programName = `Test Program for New Campaign ${Date.now()}`; + programName = faker.company.name() page = await browser.newPage() await doLogin(page) await createProgram(page, programName) }) + test('Can create a campaign', async () => { await page.goto('http://localhost:4000/campaigns/new'); + await page.waitForSelector("body > .phx-connected") await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); - // TODO: this won't select option by label, meaning it's selecting an arbitrary program. - await page.locator('#user-form_program_id').selectOption("3"); // << this works but isn't ideal + await page.locator('#user-form_program_id').click() + // HACK: a way to target the specific program, since we can't select by value for some reason. + await page.locator('#user-form_program_id').pressSequentially(programName, { delay: 30 }) + await page.locator('#user-form_program_id').press('Enter') + + await page.locator('#location-form-location-input-open').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); - await page.waitForTimeout(2000); + await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully'); + await expect(page.getByText(programName)).toBeVisible(); }) test('Can create a campaign for next week', async () => { await page.goto('http://localhost:4000/campaigns/new'); + await page.waitForSelector("body > .phx-connected") await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); - // TODO: this won't select option by label, meaning it's selecting an arbitrary program. - await page.locator('#user-form_program_id').selectOption({label: programName}); - // await page.locator('#user-form_program_id').selectOption("3"); + await page.locator('#user-form_program_id').click() + await page.locator('#user-form_program_id').pressSequentially(programName, { delay: 30 }) + await page.locator('#user-form_program_id').press('Enter') await page.locator('#location-form-location-input-open').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').click(); - await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); - await page.waitForTimeout(2000); + await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully'); + // now go check that campaign shows up next week. await page.getByRole('link', { name: 'Campaigns' }).click(); // goto next week await page.getByRole('navigation', { name: 'Pagination' }).getByRole('link').nth(2).click(); // ensure that new campaign for next-week is present - await expect(page.locator('#campaign-4')).toContainText('Test Program 1739811044662 edited'); - await expect(page.getByRole('link', { name: 'Test Program 1739811044662' })).toBeVisible(); + await expect(page.getByText(programName)).toBeVisible(); }) }) @@ -116,22 +122,6 @@ async function doLogin(page: any) { } -// async function createCampaign(page: any) { -// await page.getByRole('link', { name: 'Campaigns' }).click(); -// await page.getByRole('link', { name: 'New Campaign' }).click(); -// await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); -// await page.locator('#user-form_program_id').selectOption({}); -// await page.locator('#location-form-location-input-open').click(); -// await page.locator('[id="campaign_form\\[location\\]_address"]').click(); -// await page.locator('[id="campaign_form\\[location\\]_address"]').fill('123 yonge'); -// await page.waitForTimeout(2000); -// await page.getByRole('button', { name: 'Save' }).click(); -// await expect(page.getByText('Success! Campaign created')).toBeVisible({ timeout: 10000 }); -// } - -// async function createCampaignForNextWeek(page: any) { } - - function getDatePlusDays(daysToAdd: number) { const today = new Date(); const futureDate = new Date(today); diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json index 01a421a0..72f7e2cf 100644 --- a/test/e2e/package-lock.json +++ b/test/e2e/package-lock.json @@ -9,10 +9,28 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@faker-js/faker": "^9.5.0", "@playwright/test": "^1.50.1", "@types/node": "^22.13.1" } }, + "node_modules/@faker-js/faker": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.0.tgz", + "integrity": "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@playwright/test": { "version": "1.50.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", diff --git a/test/e2e/package.json b/test/e2e/package.json index 960464b6..805cf233 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -12,7 +12,8 @@ "license": "ISC", "description": "", "devDependencies": { + "@faker-js/faker": "^9.5.0", "@playwright/test": "^1.50.1", "@types/node": "^22.13.1" } -} \ No newline at end of file +} From 599ca17d30664fb67e5fdf24bd1a6066c1f2a74f Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 17 Mar 2025 11:49:59 +0000 Subject: [PATCH 05/10] Add labels to select options for better test targeting. --- .../live/campaign_live/form_component.ex | 2 +- test/e2e/login.spec.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/bike_brigade_web/live/campaign_live/form_component.ex b/lib/bike_brigade_web/live/campaign_live/form_component.ex index b2ec1374..5f6d608a 100644 --- a/lib/bike_brigade_web/live/campaign_live/form_component.ex +++ b/lib/bike_brigade_web/live/campaign_live/form_component.ex @@ -72,7 +72,7 @@ defmodule BikeBrigadeWeb.CampaignLive.FormComponent do @impl true def mount(socket) do - programs = for p <- Delivery.list_programs(), do: {p.name, p.id} + programs = for p <- Delivery.list_programs(), do: [key: p.name, value: p.id, label: p.name] {:ok, socket diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts index a32aaa8a..8ff9aeaf 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/login.spec.ts @@ -43,7 +43,7 @@ test.describe('Programs', () => { await doLogin(page) // only needs to run once per describe block await page.goto('http://localhost:4000/programs'); - createProgram(page, programName) + await createProgram(page, programName) await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible(); // editing of program @@ -77,10 +77,9 @@ test.describe('Campaigns', () => { await page.goto('http://localhost:4000/campaigns/new'); await page.waitForSelector("body > .phx-connected") await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); - await page.locator('#user-form_program_id').click() - // HACK: a way to target the specific program, since we can't select by value for some reason. - await page.locator('#user-form_program_id').pressSequentially(programName, { delay: 30 }) - await page.locator('#user-form_program_id').press('Enter') + + const programSelector = page.locator('#user-form_program_id') + await programSelector.selectOption({label: programName}) await page.locator('#location-form-location-input-open').click(); @@ -94,11 +93,12 @@ test.describe('Campaigns', () => { await page.goto('http://localhost:4000/campaigns/new'); await page.waitForSelector("body > .phx-connected") await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); - await page.locator('#user-form_program_id').click() - await page.locator('#user-form_program_id').pressSequentially(programName, { delay: 30 }) - await page.locator('#user-form_program_id').press('Enter') + + const programSelector = page.locator('#user-form_program_id') + await programSelector.selectOption({label: programName}) + await page.locator('#location-form-location-input-open').click(); - await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) + await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 200 }) await page.getByRole('button', { name: 'Save' }).click(); await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully'); From 4e3976887fd6e751336f6dde44389517949079f4 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 19 Mar 2025 14:15:04 +0000 Subject: [PATCH 06/10] Fix typo --- .../live/program_live/form_component.html.heex | 2 +- test-results/.last-run.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test-results/.last-run.json diff --git a/lib/bike_brigade_web/live/program_live/form_component.html.heex b/lib/bike_brigade_web/live/program_live/form_component.html.heex index 661f21ef..3a6cd897 100644 --- a/lib/bike_brigade_web/live/program_live/form_component.html.heex +++ b/lib/bike_brigade_web/live/program_live/form_component.html.heex @@ -147,7 +147,7 @@ <.input type="text" field={f[:photo_description]} - label="Photo Descriotion" + label="Photo Description" placeholder="Typical delivery size" />
diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 00000000..5fca3f84 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file From 1635c40671b2f7fabbdf42e4b674322821d5c686 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 19 Mar 2025 14:36:33 +0000 Subject: [PATCH 07/10] Refactor: createCampaign to func; add to createProgram --- README.md | 29 ++++++++++++++++---- mix.exs | 3 ++- test/e2e/login.spec.ts | 60 +++++++++++++++++++++++++----------------- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 6b473347..eba37063 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,29 @@ Copyright 2021 The Bike Brigade Inc. See the License for the specific language governing permissions and limitations under the License. - ## End to End testing with Playwright +## End to End testing with Playwright - To run e2e tests: +To run e2e tests: - 1. navigate to `/test/e2e` and run `npm install` - 1. from the root directory, run `mix test.e2e` - (THIS WILL WIPE YOUR LOCAL DB AND RESET IT) - 1. in a neew terminal, navigate to `/test/e2e` and run `npm run test:ui` +1. navigate to `/test/e2e` and run `npm install` +1. from the root directory, run `mix test.e2e` - (THIS WILL WIPE YOUR LOCAL DB AND RESET IT) +1. in a new terminal, navigate to `/test/e2e` and run `npm run test:ui` + +### Troubleshooting E2E tests + +Sometimes e2e tests will fail due to network calls that are actually being made. For example, calling Google Maps to fetch addresses when creating a campaign. + + +Often, the best thing you can do is re-run the failed individual tests and see if they pass. If not, you may need to go and tweak the delays of certain statements in the test, for example: + +```js +await page + .locator('#location-form-location-input-open') + .pressSequentially("200 Yonge", { delay: 100 }) + +// becomes... + +await page + .locator('#location-form-location-input-open') + .pressSequentially("200 Yonge", { delay: 200 }) +``` diff --git a/mix.exs b/mix.exs index cc7b1310..4b226378 100644 --- a/mix.exs +++ b/mix.exs @@ -124,7 +124,8 @@ defmodule BikeBrigade.MixProject do "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"], "assets.deploy": ["esbuild default --minify", "tailwind default --minify", "phx.digest"], - "test.e2e": ["ecto.reset", "phx.server"] + # REVIEW: Sometimes this hangs on a query - `INSERT INTO "tasks_items"...` + "test.e2e": ["ecto.drop", "phx.server"] ] end diff --git a/test/e2e/login.spec.ts b/test/e2e/login.spec.ts index 8ff9aeaf..a61526b5 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/login.spec.ts @@ -74,33 +74,13 @@ test.describe('Campaigns', () => { test('Can create a campaign', async () => { - await page.goto('http://localhost:4000/campaigns/new'); - await page.waitForSelector("body > .phx-connected") - await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(0)); - - const programSelector = page.locator('#user-form_program_id') - await programSelector.selectOption({label: programName}) - - - await page.locator('#location-form-location-input-open').click(); - await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) - await page.getByRole('button', { name: 'Save' }).click(); + await createCampaign({page, programName, numDays: 0}) await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully'); await expect(page.getByText(programName)).toBeVisible(); }) test('Can create a campaign for next week', async () => { - await page.goto('http://localhost:4000/campaigns/new'); - await page.waitForSelector("body > .phx-connected") - await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(8)); - - const programSelector = page.locator('#user-form_program_id') - await programSelector.selectOption({label: programName}) - - await page.locator('#location-form-location-input-open').click(); - await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 200 }) - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully'); + await createCampaign({page, programName, numDays: 8}) // now go check that campaign shows up next week. await page.getByRole('link', { name: 'Campaigns' }).click(); @@ -148,8 +128,9 @@ async function createProgram(page: Page, programName: string) { await page.getByRole('checkbox', { name: 'Public' }).check(); await page.getByRole('checkbox', { name: 'Hide Pickup Address' }).check(); await page.getByRole('button', { name: 'Add Schedule' }).click(); - await page.getByRole('textbox', { name: 'Photo Descriotion' }).click(); - await page.getByRole('textbox', { name: 'Photo Descriotion' }).fill('1 Large Box'); + + await page.getByRole('textbox', { name: 'Photo Description' }).click(); + await page.getByRole('textbox', { name: 'Photo Description' }).fill('1 Large Box'); await page.getByRole('textbox', { name: 'Contact Name' }).click(); await page.getByRole('textbox', { name: 'Contact Name' }).fill('Joe Cool'); await page.getByRole('textbox', { name: 'Contact Name' }).press('Tab'); @@ -157,4 +138,35 @@ async function createProgram(page: Page, programName: string) { await page.getByRole('textbox', { name: 'Contact Email' }).press('Tab'); await page.getByRole('textbox', { name: 'Contact Phone' }).fill('6475555554'); await page.getByRole('button', { name: 'Save' }).click(); + + + // once the program is created, we need to find it on the program page and EDIT it, + // because at this time there is no "add items" for part when creating a new program + // (only when editing it) + await page.getByRole('link', { name: programName, exact: true }).click(); + await page.getByRole('link', { name: 'Edit', exact: true }).click(); + await page.getByRole('link', { name: 'New Item' }).click(); + await page.locator('#program-form_program_0_items_0_name').click(); + await page.locator('#program-form_program_0_items_0_name').fill('An item'); + await page.locator('#program-form_program_0_items_0_name').press('Tab'); + await page.locator('#program-form_program_0_items_0_description').fill('5 lbs'); + await page.getByRole('cell', { name: 'Foodshare Box' }).getByLabel('').selectOption('Food Hamper'); + await page.getByRole('button', { name: 'Save' }).click(); + // return to programs because otherwise we'll be on programs/ + await page.goto('http://localhost:4000/programs'); + +} + +async function createCampaign({ page, programName, numDays }: any) { + await page.goto('http://localhost:4000/campaigns/new'); + await page.waitForSelector("body > .phx-connected") + await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(numDays)); + + const programSelector = page.locator('#user-form_program_id') + await programSelector.selectOption({ label: programName }) + + + await page.locator('#location-form-location-input-open').click(); + await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) + await page.getByRole('button', { name: 'Save' }).click(); } From 37c8a24fcdf6fdfdca0253b7b1d96eeedc8dd618 Mon Sep 17 00:00:00 2001 From: Ty Date: Thu, 27 Mar 2025 15:24:02 -0400 Subject: [PATCH 08/10] Rename file. --- test/e2e/{login.spec.ts => all.spec.ts} | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename test/e2e/{login.spec.ts => all.spec.ts} (98%) diff --git a/test/e2e/login.spec.ts b/test/e2e/all.spec.ts similarity index 98% rename from test/e2e/login.spec.ts rename to test/e2e/all.spec.ts index a61526b5..3f095dec 100644 --- a/test/e2e/login.spec.ts +++ b/test/e2e/all.spec.ts @@ -39,6 +39,8 @@ test.describe('Login and Logout', () => { test.describe('Programs', () => { + + // TODO: something about this is failing on a fresh db. test('Can create and edit program', async ({ page }) => { await doLogin(page) // only needs to run once per describe block await page.goto('http://localhost:4000/programs'); @@ -54,6 +56,7 @@ test.describe('Programs', () => { await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText('Success! program updated')).toBeVisible(); + await page.getByRole('link', { name: `Edit , ${programName}` }).click(); await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).click(); await expect(page.getByLabel('Campaign Blurb (please keep')).toContainText('This is a test program that was updated'); @@ -167,6 +170,6 @@ async function createCampaign({ page, programName, numDays }: any) { await page.locator('#location-form-location-input-open').click(); - await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 100 }) + await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 200 }) await page.getByRole('button', { name: 'Save' }).click(); } From 742fe635596ea8c843bbbd521c252406412aa0e4 Mon Sep 17 00:00:00 2001 From: Ty Date: Thu, 27 Mar 2025 15:30:04 -0400 Subject: [PATCH 09/10] A few small notes. --- README.md | 2 +- test/e2e/all.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eba37063..6a9a7b28 100644 --- a/README.md +++ b/README.md @@ -120,13 +120,13 @@ To run e2e tests: 1. navigate to `/test/e2e` and run `npm install` 1. from the root directory, run `mix test.e2e` - (THIS WILL WIPE YOUR LOCAL DB AND RESET IT) +1. If this command fails, you can run `mix ecto.reset && mix phx.server`, again, this will WIPE YOUR LOCAL DB AND RESET IT. 1. in a new terminal, navigate to `/test/e2e` and run `npm run test:ui` ### Troubleshooting E2E tests Sometimes e2e tests will fail due to network calls that are actually being made. For example, calling Google Maps to fetch addresses when creating a campaign. - Often, the best thing you can do is re-run the failed individual tests and see if they pass. If not, you may need to go and tweak the delays of certain statements in the test, for example: ```js diff --git a/test/e2e/all.spec.ts b/test/e2e/all.spec.ts index 3f095dec..48e871f7 100644 --- a/test/e2e/all.spec.ts +++ b/test/e2e/all.spec.ts @@ -40,7 +40,7 @@ test.describe('Login and Logout', () => { test.describe('Programs', () => { - // TODO: something about this is failing on a fresh db. + // NOTE: something about this is sometimes fails on a fresh db. test('Can create and edit program', async ({ page }) => { await doLogin(page) // only needs to run once per describe block await page.goto('http://localhost:4000/programs'); From b05ebda291efb9d6c8d1037da98189f98c44cbd7 Mon Sep 17 00:00:00 2001 From: Ty Date: Thu, 10 Apr 2025 15:55:38 -0400 Subject: [PATCH 10/10] start setting up sandbox again. --- config/dev.exs | 4 ++- lib/bike_brigade_web/endpoint.ex | 20 +++++++++++ test/e2e/all.spec.ts | 3 +- test/e2e/helpers/sandbox.ts | 57 ++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 test/e2e/helpers/sandbox.ts diff --git a/config/dev.exs b/config/dev.exs index 1e96b884..1289dbc4 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -10,7 +10,8 @@ config :bike_brigade, BikeBrigade.Repo, database: "bike_brigade_dev", hostname: "localhost", show_sensitive_data_on_connection_error: true, - pool_size: 10 + pool_size: 10, + pool: Ecto.Adapters.SQL.Sandbox # For development, we disable any cache and enable # debugging and code reloading. @@ -66,6 +67,7 @@ config :bike_brigade, BikeBrigadeWeb.Endpoint, # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" +# config :logger, level: :warning # Disable the extremely annoying debug logging for the spreadsheet library config :logger, diff --git a/lib/bike_brigade_web/endpoint.ex b/lib/bike_brigade_web/endpoint.ex index 1d7e05c3..7b7b4899 100644 --- a/lib/bike_brigade_web/endpoint.ex +++ b/lib/bike_brigade_web/endpoint.ex @@ -1,5 +1,7 @@ defmodule BikeBrigadeWeb.Endpoint do use Phoenix.Endpoint, otp_app: :bike_brigade + require Logger + if sandbox = Application.compile_env(:bike_brigade, :sandbox) do plug Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox @@ -21,6 +23,24 @@ defmodule BikeBrigadeWeb.Endpoint do socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + + defp log_sandbox_requests(conn, _opts) do + if conn.request_path == "/sandbox" do + Logger.info("2@@@@@@@@@@@@@@@@@@@@@ SANDBOX REQUEST: #{conn.method} #{conn.request_path}") + end + conn + end + + plug :log_sandbox_requests + + plug Phoenix.Ecto.SQL.Sandbox, + at: "/sandbox", + header: "x-session-id", + repo: BikeBrigade.Repo, + timeout: 15_000 # the default + + + # Serve at "/static" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest diff --git a/test/e2e/all.spec.ts b/test/e2e/all.spec.ts index 48e871f7..01e6f461 100644 --- a/test/e2e/all.spec.ts +++ b/test/e2e/all.spec.ts @@ -1,4 +1,5 @@ -import { test, expect, Page } from '@playwright/test'; +import { Page } from '@playwright/test'; +import { test, expect } from "./helpers/sandbox"; import { faker } from '@faker-js/faker'; const programName = faker.company.name() diff --git a/test/e2e/helpers/sandbox.ts b/test/e2e/helpers/sandbox.ts new file mode 100644 index 00000000..b972424e --- /dev/null +++ b/test/e2e/helpers/sandbox.ts @@ -0,0 +1,57 @@ +// tests/helpers/sandbox.js +import { request, test as base } from '@playwright/test'; + +async function setupSandbox(context: any) { + // Create sandbox + const requestContext = await request.newContext(); + const response = await requestContext.post('http://localhost:4000/sandbox', { + headers: { + 'Cache-Control': 'no-store' + } + }); + + const sessionId = await response.text(); + + // Set up the route interception to add sessionId to all requests + await context.route('**/*', async (route: any, request: any) => { + const headers = request.headers(); + headers['x-session-id'] = sessionId; + await route.continue({ headers }); + }); + + // Store the sessionId for LiveView connections + await context.addInitScript(({ sessionId }) => { + window.sessionId = sessionId; + }, { sessionId }); + + return sessionId; +} + +async function teardownSandbox(sessionId: any) { + const requestContext = await request.newContext(); + await requestContext.delete('http://localhost:4000/sandbox', { + headers: { + 'x-session-id': sessionId + } + }); +} + +// module.exports = { setupSandbox, teardownSandbox }; + + + +const test = base.extend({ + context: async ({ context }, use) => { + const sessionId = await setupSandbox(context); + console.log("sessionId is >>>>>>>>>>: ", sessionId) + await use(context); + await teardownSandbox(sessionId); + } +}); + +const expect = base.expect + +export { + test, + expect +}