Skip to content

Commit

Permalink
Frontend testing framework (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
NivRichter authored May 22, 2023
1 parent e9b0ea6 commit cfc3e10
Show file tree
Hide file tree
Showing 9 changed files with 1,072 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/test_frontend_react.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Playwright Tests
on:
pull_request:

jobs:
test:
timeout-minutes: 6
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Run checks
uses: actions/setup-node@v3
with:
node-version: "16"
cache: "yarn"
cache-dependency-path: .framework/react/frontend

- name: Install dependencies
run: yarn install
working-directory: .framework/react/frontend

- name: Run frontend client
run: WILCO_ID=0 REACT_APP_BACKEND_URL=http://localhost:3001 yarn start >& /dev/null &
working-directory: .framework/react/frontend

- name: Install test deps
run: yarn install
working-directory: tests/frontend

- name: Run Playwright tests
run: yarn test
working-directory: tests/frontend
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
/frontend/node_modules
/.wilco-helpers/node_modules
/tests/e2e/node_modules
/tests/frontend/node_modules/
/tests/frontend/test-results/
/tests/frontend/playwright-report/
/tests/frontend/playwright/.cache/

/.pnp
.pnp.js

Expand Down
5 changes: 5 additions & 0 deletions tests/frontend/global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = async (config) => {
process.env.REACT_APP_URL = "http://localhost:3000";
process.env.BACKEND_URL = "http://localhost:3001";
process.env.BACKEND_API_URL = `${process.env.BACKEND_URL}/api`;
};
17 changes: 17 additions & 0 deletions tests/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "frontend",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@playwright/test": "^1.30.0",
"eslint": "^8.34.0",
"playwright": "^1.30.0",
"prettier": "^2.8.4",
"uid": "^2.0.1"
},
"scripts": {
"test": "playwright test --config=playwright.config.js",
"format": "yarn prettier --write ."
}
}
67 changes: 67 additions & 0 deletions tests/frontend/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @ts-check
const { defineConfig, devices } = require("@playwright/test");

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
globalSetup: "./global-setup.js",
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* 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: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost: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'] },
// },
],

/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
});
68 changes: 68 additions & 0 deletions tests/frontend/requestValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const EventEmitter = require("events");
const { sleep } = require("../utils");
const { expect } = require("@playwright/test");
const { uid } = require("uid");

const eventEmitter = new EventEmitter();

const dispatch = (type) => {
eventEmitter.emit(type);
};

const subscribe = (type, callback) => {
eventEmitter.on(type, callback);
};

const unsubscribe = (type, callback) => {
eventEmitter.removeListener(type, callback);
};

const wrapWithRequestId = (func) => {
return () => {
const requestId = uid();
func(requestId);
return requestId;
};
};

const listenAndTriggerRequest = async (
requestListenerCallback,
requestTrigger
) => {
const requestId = await requestListenerCallback();
await execAndWaitForRequest(requestId, requestTrigger);
};

const execAndWaitForRequest = async (requestId, func, maxTime = 500) => {
let eventPromise;
const eventCallback = () => {
eventPromise(`The event ${requestId} was sent to Wilco`);
};

const subscribePromiseAndExec = new Promise(async (resolve) => {
subscribe(requestId, eventCallback);
eventPromise = resolve;
await func();
});

try {
const result = await Promise.race([
new Promise((resolve) => setTimeout(resolve, maxTime)),
subscribePromiseAndExec,
]);
expect(result).toBe(`The event ${requestId} was sent to Wilco`);
} catch {
throw new Error(`The event ${requestId} was not sent to Wilco`);
} finally {
unsubscribe(requestId, eventCallback);
}
};

module.exports = {
dispatch,
subscribe,
unsubscribe,
wrapWithRequestId,
listenAndTriggerRequest,
execAndWaitForRequest,
};
81 changes: 81 additions & 0 deletions tests/frontend/tests/editor.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, test } from "@playwright/test";

import {
dispatch,
listenAndTriggerRequest,
wrapWithRequestId,
} from "../requestValidator";

const title = "title";
const description = "description";
const imageUrl =
"https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";

const item = {
item: {
slug: `xxxxx-${title}`,
title: title,
description: description,
image: imageUrl,
createdAt: "2023-02-21T19:33:06.752Z",
updatedAt: "2023-02-21T19:33:06.752Z",
tagList: [],
favorited: false,
favoritesCount: 0,
seller: {
username: "username",
image: "https://static.productionready.io/images/smiley-cyrus.jpg",
},
},
};
const wrapExpectCreateItem = (page, title, description, image) => {
return wrapWithRequestId((requestId) => {
page.on("request", (request) => {
if (
request.url() === `${process.env.BACKEND_API_URL}/items` &&
request.method() === "POST"
) {
expect(JSON.parse(request.postData())?.item?.title).toEqual(title);
expect(JSON.parse(request.postData())?.item?.description).toEqual(
description
);
expect(JSON.parse(request.postData())?.item?.image).toEqual(image);
dispatch(requestId);
}
});
})();
};

test.beforeEach(async ({ page }) => {
await page.route(`${process.env.BACKEND_API_URL}/items`, async (route) => {
const json = item;
await route.fulfill({ json, contentType: "application/json", status: 200 });
});
});

test("Creating an item trigger a request", async ({ page }) => {
const title = "title";
const description = "description";
const imageUrl =
"https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";
await page.goto(`${process.env.REACT_APP_URL}/editor`);
await page.getByPlaceholder("Item Title").fill(title);
await page.getByPlaceholder("What's this item about?").fill(description);
await page.getByPlaceholder("Image url").fill(imageUrl);
await listenAndTriggerRequest(
async () => wrapExpectCreateItem(page, title, description, imageUrl),
async () => await page.getByRole("button", { name: "Publish Item" }).click()
);
});

test("Creating an item redirects to the item page", async ({ page }) => {
await page.goto(`${process.env.REACT_APP_URL}/editor`);
await page.getByPlaceholder("Item Title").fill(title);
await page.getByPlaceholder("What's this item about?").fill(description);
await page.getByPlaceholder("Image url").fill(imageUrl);
await page.getByRole("button", { name: "Publish Item" }).click();
await page.waitForURL(`${process.env.REACT_APP_URL}/item/*`);
expect(page.url()).toEqual(
`${process.env.REACT_APP_URL}/item/${item.item.slug}`
);
});
99 changes: 99 additions & 0 deletions tests/frontend/tests/signup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect, test } from "@playwright/test";

import {
dispatch,
listenAndTriggerRequest,
wrapWithRequestId,
} from "../requestValidator";
import { uid } from "uid";

const username = `user${(Math.random() + 1).toString(36).substring(7)}`;
const email = `${username}@email.com`;
const password = `pass${(Math.random() + 1).toString(36).substring(7)}`;
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzZWE2MjAxZjg3MjE1OGMzMTE1YWI0ZSIsInVzZXJuYW1lIjoidXNlcjJ3d3FlIiwiZXhwIjoxNjgxNDg1Mjk3LCJpYXQiOjE2NzYzMDQ4OTd9.Z45FqelGgXLU4q6xkhw_fTHZ5GXoVsx0vI_HoI3ccDo";

const wrapExpectCreateUser = (page, username, email, password) => {
return wrapWithRequestId((requestId) => {
page.on("request", (request) => {
if (
request.url() === `${process.env.BACKEND_API_URL}/users` &&
request.method() === "POST"
) {
expect(JSON.parse(request.postData())?.user?.username).toEqual(
username
);
expect(JSON.parse(request.postData())?.user?.email).toEqual(email);
expect(JSON.parse(request.postData())?.user?.password).toEqual(
password
);
dispatch(requestId);
}
});
})();
};

test.beforeEach(async ({ page }) => {
await page.route(`${process.env.BACKEND_API_URL}/users`, async (route) => {
const json = {
user: {
username: username,
email: email,
token: token,
role: "user",
},
};
await route.fulfill({ json, contentType: "application/json", status: 200 });
});

await page.route(
`${process.env.BACKEND_API_URL}/profiles/${username}`,
async (route) => {
const json = {
profile: {
username: username,
image: "https://static.productionready.io/images/smiley-cyrus.jpg",
following: false,
},
};
await route.fulfill({
json,
headers: { "Content-Type": "application/json" },
});
}
);

await page.route(
`${process.env.BACKEND_API_URL}/items?seller=${username}**`,
async (route) => {
const json = { items: [], itemsCount: 0 };
await route.fulfill({
json,
headers: { "Content-Type": "application/json" },
});
}
);
});

test("User creation redirects to profile page ", async ({ page }) => {
await page.goto(`${process.env.REACT_APP_URL}`);
await page.getByRole("link", { name: "Sign up" }).click();
await page.getByPlaceholder("Username").fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByPlaceholder("Email").fill(email);
await page.getByRole("button", { name: "SIGN UP" }).click();
await page.waitForSelector(`[href="/@${username}"]`);
expect(page.url()).toBe(`${process.env.REACT_APP_URL}/@${username}`);
});

test("Creating a user triggers a request", async ({ page }) => {
await page.goto(`${process.env.REACT_APP_URL}`);
await page.getByRole("link", { name: "Sign up" }).click();
await page.getByPlaceholder("Username").fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByPlaceholder("Email").fill(email);
await listenAndTriggerRequest(
async () => wrapExpectCreateUser(page, username, email, password),
async () => await page.getByRole("button", { name: "SIGN UP" }).click()
);
});
Loading

0 comments on commit cfc3e10

Please sign in to comment.