Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
```
4 changes: 3 additions & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions lib/bike_brigade_web/endpoint.ex
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/bike_brigade_web/live/campaign_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was required in order to add "label" attributes to options in the <select> tag; these labels are used to traget dropdowns in tests.


{:ok,
socket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
<.input
type="text"
field={f[:photo_description]}
label="Photo Descriotion"
label="Photo Description"
placeholder="Typical delivery size"
/>
<div class="grid grid-cols-3 gap-4 mt-2">
Expand Down
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's going on here—but it just seems to hang. Maybe just remove it?

]
end

Expand Down
4 changes: 4 additions & 0 deletions test-results/.last-run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}
7 changes: 7 additions & 0 deletions test/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
176 changes: 176 additions & 0 deletions test/e2e/all.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +147 to +149
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is an intentional part of the New Program form, or if the ability to add items should be possible in that form.

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/<program_id>
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();
}
57 changes: 57 additions & 0 deletions test/e2e/helpers/sandbox.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading