diff --git a/README.md b/README.md
index fc489c1c..6a9a7b28 100644
--- a/README.md
+++ b/README.md
@@ -113,3 +113,30 @@ 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. 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
+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/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/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/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/mix.exs b/mix.exs
index 82be0a53..4b226378 100644
--- a/mix.exs
+++ b/mix.exs
@@ -123,7 +123,9 @@ 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"],
+ # REVIEW: Sometimes this hangs on a query - `INSERT INTO "tasks_items"...`
+ "test.e2e": ["ecto.drop", "phx.server"]
]
end
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
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/all.spec.ts b/test/e2e/all.spec.ts
new file mode 100644
index 00000000..01e6f461
--- /dev/null
+++ b/test/e2e/all.spec.ts
@@ -0,0 +1,176 @@
+import { Page } from '@playwright/test';
+import { test, expect } from "./helpers/sandbox";
+import { faker } from '@faker-js/faker';
+
+const programName = faker.company.name()
+
+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');
+ });
+
+ 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', () => {
+
+
+ // 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');
+
+ await 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
+ let programName: string
+
+ test.beforeAll(async ({ browser }) => {
+ programName = faker.company.name()
+ page = await browser.newPage()
+ await doLogin(page)
+ await createProgram(page, programName)
+ })
+
+
+ test('Can create a campaign', async () => {
+ 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 createCampaign({page, programName, numDays: 8})
+
+ // 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.getByText(programName)).toBeVisible();
+ })
+})
+
+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();
+}
+
+
+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();
+ 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 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');
+ 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();
+
+
+ // 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: 200 })
+ await page.getByRole('button', { name: 'Save' }).click();
+}
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
+}
diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json
new file mode 100644
index 00000000..72f7e2cf
--- /dev/null
+++ b/test/e2e/package-lock.json
@@ -0,0 +1,115 @@
+{
+ "name": "e2e",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "e2e",
+ "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",
+ "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..805cf233
--- /dev/null
+++ b/test/e2e/package.json
@@ -0,0 +1,19 @@
+{
+ "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": {
+ "@faker-js/faker": "^9.5.0",
+ "@playwright/test": "^1.50.1",
+ "@types/node": "^22.13.1"
+ }
+}
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,
+ // },
+});