diff --git a/apps/e2e-tests/.github/workflows/playwright.yml b/apps/e2e-tests/.github/workflows/playwright.yml new file mode 100644 index 0000000..6fc4819 --- /dev/null +++ b/apps/e2e-tests/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Install dependencies + run: pnpm install + - name: Install Playwright Browsers + run: pnpm dlx playwright install --with-deps + - name: Run Playwright tests + run: pnpm dlx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/apps/e2e-tests/.gitignore b/apps/e2e-tests/.gitignore new file mode 100644 index 0000000..75e854d --- /dev/null +++ b/apps/e2e-tests/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/apps/e2e-tests/configuration.ts b/apps/e2e-tests/configuration.ts new file mode 100644 index 0000000..d8729b3 --- /dev/null +++ b/apps/e2e-tests/configuration.ts @@ -0,0 +1,16 @@ +import appUrls from "./urls/app-urls"; +import saleorUrls from "./urls/saleor-urls"; + +export const appName = process.env.APP_NAME; // TODO: name should be taken from the manifest to eliminate possible footguns +export const appUrl = process.env.APP_URL; +export const instanceUrl = process.env.INSTANCE_URL; +export const dashboardUserEmail = process.env.DASHBOARD_USER_EMAIL; +export const dashboardUserPassword = process.env.DASHBOARD_USER_PASSWORD; + +export const urls = { + app: { + baseUrl: appUrl, + ...appUrls(appUrl) + }, + saleor: saleorUrls(instanceUrl) +} diff --git a/apps/e2e-tests/package.json b/apps/e2e-tests/package.json new file mode 100644 index 0000000..200a249 --- /dev/null +++ b/apps/e2e-tests/package.json @@ -0,0 +1,13 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.29.2" + } +} diff --git a/apps/e2e-tests/playwright.config.ts b/apps/e2e-tests/playwright.config.ts new file mode 100644 index 0000000..15f5e3c --- /dev/null +++ b/apps/e2e-tests/playwright.config.ts @@ -0,0 +1,100 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + 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'], + // }, + // }, + + /* 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: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* 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, + // }, +}; + +export default config; diff --git a/apps/e2e-tests/tests/example.spec.ts b/apps/e2e-tests/tests/example.spec.ts new file mode 100644 index 0000000..a6460d4 --- /dev/null +++ b/apps/e2e-tests/tests/example.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import { appName, urls } from '../configuration'; +import { checkIfAppIsAvailable } from '../utils/check-if-app-is-available'; +import { installTheApp } from '../utils/install-the-app'; +import { logInIntoDashboard } from '../utils/log-in-into-dashboard'; +import { randomString } from '../utils/random-string'; + +test("The app can be installed", async ({page}) => { + // TODO: add function to completely remove all the installed apps in the test env + // alternative: make locators tighter to restrict it only to the tested app + await logInIntoDashboard({page}) + await installTheApp({page}) + await checkIfAppIsAvailable({page}) +}) + +test("Smoke test of the configuration", async ({page}) => { + await logInIntoDashboard({page}) + + // open app configuration view + await page.goto(urls.saleor.dashboard.apps, {timeout: 20000, waitUntil: "load"}); + await page.getByText(appName).first().click() + + // generate unique marker to ensure values are updated during this test run + const marker = randomString() + + // fill the configuration fields + await page.frameLocator('iframe').locator('input[name="secretKey"]').click(); + await page.frameLocator('iframe').locator('input[name="secretKey"]').fill('secret-key-'+marker); + await page.frameLocator('iframe').locator('input[name="searchKey"]').click(); + await page.frameLocator('iframe').locator('input[name="searchKey"]').fill('search-key-'+marker); + await page.frameLocator('iframe').locator('input[name="appId"]').click(); + await page.frameLocator('iframe').locator('input[name="appId"]').fill('app-id-'+marker); + await page.frameLocator('iframe').locator('input[name="indexNamePrefix"]').click(); + await page.frameLocator('iframe').locator('input[name="indexNamePrefix"]').fill('prefix-'+marker); + + // submit and wait for the confirmation toast + await page.mouse.wheel(0, 20); // TODO: investigate how to automatically scroll + await page.frameLocator('iframe').getByRole('button', { name: 'Save' }).click(); + await page.getByText('Configuration saved!').isVisible(); + + // check if the data persists + await page.goto(urls.saleor.dashboard.apps, {timeout: 20000, waitUntil: "load"}); + await page.getByText(appName).first().click() + + // check if the form data loaded + await page.frameLocator('iframe').getByRole('button', { name: 'Save' }).isEnabled(); + + // check field values + await expect(await page.frameLocator('iframe').locator('input[name="secretKey"]').inputValue()).toBe("secret-key-"+marker); + await expect(await page.frameLocator('iframe').locator('input[name="searchKey"]').inputValue()).toBe("search-key-"+marker); + await expect(await page.frameLocator('iframe').locator('input[name="appId"]').inputValue()).toBe("app-id-"+marker); + await expect(await page.frameLocator('iframe').locator('input[name="indexNamePrefix"]').inputValue()).toBe("prefix-"+marker); +}) diff --git a/apps/e2e-tests/types.ts b/apps/e2e-tests/types.ts new file mode 100644 index 0000000..bfe552f --- /dev/null +++ b/apps/e2e-tests/types.ts @@ -0,0 +1,17 @@ +export interface WebhookManifest { + name: string; + targetUrl: string; + asyncEvents: string[]; + isActive: boolean; + query: string; +} + +export interface Manifest { + id: string; + version: string; + name: string; + permissions: string[]; + appUrl: string; + tokenTargetUrl: string; + webhooks: WebhookManifest[]; +} \ No newline at end of file diff --git a/apps/e2e-tests/urls/app-urls.ts b/apps/e2e-tests/urls/app-urls.ts new file mode 100644 index 0000000..7df496f --- /dev/null +++ b/apps/e2e-tests/urls/app-urls.ts @@ -0,0 +1,7 @@ + +export const appUrls = (appUrl: string) => ({ + manifest: new URL('/api/manifest', appUrl).href, + register: new URL('/api/register', appUrl).href +}) + +export default appUrls \ No newline at end of file diff --git a/apps/e2e-tests/urls/saleor-urls.ts b/apps/e2e-tests/urls/saleor-urls.ts new file mode 100644 index 0000000..069a922 --- /dev/null +++ b/apps/e2e-tests/urls/saleor-urls.ts @@ -0,0 +1,9 @@ +export const saleorUrls = (url: string) => ({ + dashboard: { + homepage: new URL('/dashboard', url).href, + apps: new URL('/dashboard/apps', url).href + }, + api: new URL('/graphql/', url).href +}) + +export default saleorUrls \ No newline at end of file diff --git a/apps/e2e-tests/utils/check-if-app-is-available.ts b/apps/e2e-tests/utils/check-if-app-is-available.ts new file mode 100644 index 0000000..110e478 --- /dev/null +++ b/apps/e2e-tests/utils/check-if-app-is-available.ts @@ -0,0 +1,17 @@ +import { expect, Page } from "@playwright/test"; +import { appName, urls } from "../configuration"; + +interface checkIfAppIsAvailableArgs { + page: Page +} + +export const checkIfAppIsAvailable = async ({page}: checkIfAppIsAvailableArgs) => { +// got to Apps page, assuming user is logged in + await page.goto(urls.saleor.dashboard.apps, {timeout: 20000, waitUntil: "load"}); + + // look for a entry with name of our app + await expect(await page.getByText(appName).first()).toBeVisible() + + // and confirm its installed + await expect(await page.getByText('Problem occured during installation.')).toBeHidden() +} diff --git a/apps/e2e-tests/utils/delay.ts b/apps/e2e-tests/utils/delay.ts new file mode 100644 index 0000000..45fd631 --- /dev/null +++ b/apps/e2e-tests/utils/delay.ts @@ -0,0 +1,5 @@ +export const delay = (time) => { + return new Promise(function(resolve) { + setTimeout(resolve, time) + }); +} diff --git a/apps/e2e-tests/utils/install-the-app.ts b/apps/e2e-tests/utils/install-the-app.ts new file mode 100644 index 0000000..a70c2cf --- /dev/null +++ b/apps/e2e-tests/utils/install-the-app.ts @@ -0,0 +1,22 @@ +import { Page } from "@playwright/test"; +import { urls } from "../configuration"; + +interface InstallTheAppArgs { + page: Page +} + +export const installTheApp = async ({page}: InstallTheAppArgs) => { +// got to Apps page, assuming user is logged in + await page.goto(urls.saleor.dashboard.apps, {timeout: 20000, waitUntil: "load"}); + +// Install the app via the manifest URL + await page.locator('[data-test-id="add-app-from-manifest"]').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill(urls.app.manifest); + await page.locator('[data-test-id="install-app-from-manifest"]').click(); + await page.getByRole('button', { name: 'Install App' }).click(); + + // wait for the toast + await page.getByText('App installed').isVisible(); + +} diff --git a/apps/e2e-tests/utils/log-in-into-dashboard.ts b/apps/e2e-tests/utils/log-in-into-dashboard.ts new file mode 100644 index 0000000..4c98110 --- /dev/null +++ b/apps/e2e-tests/utils/log-in-into-dashboard.ts @@ -0,0 +1,19 @@ +import { Page, expect } from "@playwright/test"; +import { dashboardUserEmail, dashboardUserPassword, urls } from "../configuration"; + +interface LogInIntoDashboardArgs { + page: Page +} + +export const logInIntoDashboard = async ({page}: LogInIntoDashboardArgs) => { + await page.goto(urls.saleor.dashboard.homepage, {timeout: 20000, waitUntil: "load"}); + await page.locator('[data-test-id="email"]').click(); + await page.locator('[data-test-id="email"]').fill(dashboardUserEmail); + await page.locator('[data-test-id="email"]').press('Tab'); + await page.locator('[data-test-id="password"]').fill(dashboardUserPassword); + await page.locator('[data-test-id="submit"]').click(); + + await expect(page.locator('[data-test-id="welcome-header"]')).toBeVisible() +} + +export default logInIntoDashboard diff --git a/apps/e2e-tests/utils/random-string.ts b/apps/e2e-tests/utils/random-string.ts new file mode 100644 index 0000000..73ecab3 --- /dev/null +++ b/apps/e2e-tests/utils/random-string.ts @@ -0,0 +1 @@ +export const randomString = () => Math.random().toString(36).substr(2, 5) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196d03e..c18a2a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,6 +222,12 @@ importers: pretty-quick: 3.1.3_prettier@2.8.3 typescript: 4.8.3 + apps/e2e-tests: + specifiers: + '@playwright/test': ^1.29.2 + devDependencies: + '@playwright/test': 1.30.0 + packages/eslint-config-custom: specifiers: eslint-config-next: ^13.1.3 @@ -2373,6 +2379,15 @@ packages: tiny-glob: 0.2.9 tslib: 2.4.1 + /@playwright/test/1.30.0: + resolution: {integrity: sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@types/node': 18.11.18 + playwright-core: 1.30.0 + dev: true + /@repeaterjs/repeater/3.0.4: resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==} dev: true @@ -7689,6 +7704,12 @@ packages: thread-stream: 2.3.0 dev: false + /playwright-core/1.30.0: + resolution: {integrity: sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==} + engines: {node: '>=14'} + hasBin: true + dev: true + /png-js/1.0.0: resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} dev: false