-
Notifications
You must be signed in to change notification settings - Fork 650
Initial Cypress e2e wab test migration to Playwright #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
9ebe3a0
19d5b61
93fd23a
89da553
43da1ad
4d8db4b
bb8112c
1433ef6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| BASE_URL=http://localhost:3003 | ||
| TEST_USER_EMAIL=user2@example.com | ||
| TEST_USER_PASSWORD=!53kr3tz! |
| 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/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { expect } from "@playwright/test"; | ||
| import { test } from "../fixtures/test"; | ||
|
|
||
| import bundles from "../../cypress/bundles"; | ||
| import { Operations } from "../types/interaction"; | ||
| import { findFrameByText } from "../utils/frame"; | ||
|
|
||
| const BUNDLE_NAME = "state-management"; | ||
|
|
||
| test.describe("state-management-text-interactions", () => { | ||
| let projectId: string; | ||
| test.beforeEach(async ({ request, apiClient, page, context, env }) => { | ||
| await apiClient.login(env.testUser.email, env.testUser.password); | ||
| projectId = await apiClient.importProjectFromTemplate(bundles[BUNDLE_NAME]); | ||
|
|
||
| const cookies = await request.storageState(); | ||
|
|
||
| await context.addCookies(cookies.cookies); | ||
| await page.goto(`/projects/${projectId}`); | ||
| }); | ||
|
|
||
| test.afterEach(async ({ env, apiClient }) => { | ||
| if (projectId) { | ||
| await apiClient.removeProject( | ||
| projectId, | ||
| env.testUser.email, | ||
| env.testUser.password | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| test("can create all types of text interactions", async ({ pages, page }) => { | ||
| //GIVEN | ||
| const TEST_DATA = { | ||
| arenaName: "text interactions", | ||
| setToText: "Set to", | ||
| updateVariableAction: "updateVariable", | ||
| textVarName: "textVar", | ||
| goodbyeValue: '"goodbye"', | ||
| goodbyeText: "goodbye", | ||
| clearVariableSelector: "Clear variable", | ||
| helloText: "hello", | ||
| setToGoodbyeButtonName: 'Set to "goodbye"', | ||
| clearButtonName: "Clear", | ||
| undefinedText: "undefined", | ||
| }; | ||
| await pages.projectPage.switchArena(TEST_DATA.arenaName); | ||
|
|
||
| //WHEN | ||
| const contentFrame = await findFrameByText(page, TEST_DATA.setToText); | ||
|
||
| await contentFrame | ||
| .locator("span", { hasText: TEST_DATA.setToText }) | ||
| .first() | ||
| .click({ force: true }); | ||
| await pages.projectPage.addInteraction("onClick", { | ||
| actionName: TEST_DATA.updateVariableAction, | ||
| args: { | ||
| variable: [TEST_DATA.textVarName], | ||
| operation: Operations.NEW_VALUE, | ||
| value: TEST_DATA.goodbyeValue, | ||
| }, | ||
| }); | ||
|
|
||
| await contentFrame | ||
| .locator(`text=${TEST_DATA.clearVariableSelector}`) | ||
| .click({ force: true }); | ||
| await pages.projectPage.addInteraction("onClick", { | ||
| actionName: TEST_DATA.updateVariableAction, | ||
| args: { | ||
| variable: [TEST_DATA.textVarName], | ||
| operation: Operations.CLEAR_VALUE, | ||
| }, | ||
| }); | ||
|
|
||
| //THEN | ||
| await pages.projectPage.withinLiveMode(async (liveFrame) => { | ||
| await expect(liveFrame.locator("body")).toBeVisible(); | ||
| await expect(liveFrame.getByText(TEST_DATA.helloText)).toBeVisible(); | ||
| await liveFrame | ||
| .getByRole("button", { name: TEST_DATA.setToGoodbyeButtonName }) | ||
| .click(); | ||
| await expect( | ||
| liveFrame.getByText(TEST_DATA.goodbyeText, { exact: true }) | ||
| ).toBeVisible(); | ||
| await liveFrame | ||
| .getByRole("button", { name: TEST_DATA.clearButtonName }) | ||
| .click(); | ||
| await expect(liveFrame.getByText(TEST_DATA.undefinedText)).toBeVisible(); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { test as base } from "@playwright/test"; | ||
|
|
||
| import { ProjectPage } from "../pages/project-page"; | ||
| import { ENVIRONMENT, Environment } from "../types/environment"; | ||
| import { ApiClient } from "../utils/api-client"; | ||
|
|
||
| export interface TestFixtures { | ||
| pages: { | ||
| projectPage: ProjectPage; | ||
| }; | ||
| env: Environment; | ||
| apiClient: ApiClient; | ||
| } | ||
|
|
||
| export const test = base.extend<TestFixtures>({ | ||
| pages: async ({ page }, use) => { | ||
| const pages = { | ||
| projectPage: new ProjectPage(page), | ||
| }; | ||
| await use(pages); | ||
| }, | ||
| // eslint-disable-next-line no-empty-pattern | ||
| env: async ({}, use) => { | ||
| const environment = ENVIRONMENT; | ||
| await use(environment); | ||
| }, | ||
| apiClient: async ({ request, env }, use) => { | ||
| const client = new ApiClient(request, env.baseUrl); | ||
| await use(client); | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "playwright", | ||
| "version": "1.0.0", | ||
| "main": "index.js", | ||
| "license": "MIT", | ||
| "devDependencies": { | ||
| "@dotenvx/dotenvx": "^1.47.6", | ||
| "@playwright/test": "^1.54.0", | ||
| "@types/node": "^24.0.13" | ||
| }, | ||
| "scripts": { | ||
| "e2e": "npx dotenvx run -f .env.test -- npx playwright test" | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { Page } from "playwright/test"; | ||
|
|
||
| export class BasePage { | ||
|
||
| readonly page: Page; | ||
|
|
||
| constructor(page: Page) { | ||
| this.page = page; | ||
| } | ||
| } | ||
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { FrameLocator, Locator, Page } from "playwright/test"; | ||
|
|
||
| import { BasePage } from "./base-page"; | ||
| import { Interaction } from "../types/interaction"; | ||
|
|
||
| export class ProjectPage extends BasePage { | ||
| readonly frame = this.page | ||
| .frameLocator("iframe.studio-frame") | ||
| .frameLocator("iframe"); | ||
| readonly projectNavButton = this.frame.locator('[id="proj-nav-button"]'); | ||
| readonly projectNavClearSearchButton = this.frame.locator( | ||
| '[data-test-id="nav-dropdown-clear-search"]' | ||
| ); | ||
| readonly projectNavSearchInput = this.frame.locator( | ||
| '[data-test-id="nav-dropdown-search-input"]' | ||
| ); | ||
| readonly addInteractionButton = this.frame.locator( | ||
| '[data-test-id="add-interaction"]' | ||
| ); | ||
| readonly interactionsSearchInput = this.frame.locator("#interactions-select"); | ||
| readonly actionsDropdownButton = this.frame.locator( | ||
| '[data-plasmic-prop="action-name"]' | ||
| ); | ||
| readonly stateButton = this.frame.locator('[data-plasmic-prop="variable"]'); | ||
| readonly windowSaveButton = this.frame | ||
| .locator('[data-test-id="data-picker"]') | ||
| .locator("text=Save"); | ||
|
|
||
| readonly operationDropdownButton = this.frame.locator( | ||
| '[data-plasmic-prop="operation"]' | ||
| ); | ||
| readonly valueButton = this.frame.locator('[data-plasmic-prop="value"]'); | ||
| readonly valueCodeInput = this.frame.locator( | ||
| "div.react-monaco-editor-container" | ||
| ); | ||
| readonly closeSidebarButton = this.frame.locator( | ||
| '[data-test-id="close-sidebar-modal"]' | ||
| ); | ||
| readonly enterLiveModeButton = this.frame.locator( | ||
| '[data-test-id="enter-live-mode-btn"]' | ||
| ); | ||
| readonly liveFrame = this.page | ||
| .locator("iframe") | ||
| .first() | ||
| .contentFrame() | ||
| .locator("iframe") | ||
| .contentFrame() | ||
| .locator('[data-test-id="live-frame"]') | ||
| .contentFrame(); | ||
|
|
||
| readonly exitLiveModeButton = this.frame.locator( | ||
| '[data-test-id="exit-live-mode-btn"]' | ||
| ); | ||
|
|
||
| constructor(page: Page) { | ||
| super(page); | ||
| } | ||
|
|
||
| async selectInteractionEventById(eventHandler: string): Promise<Locator> { | ||
| const dropdownElement = this.frame.locator( | ||
| `#interactions-select-opt-${eventHandler}` | ||
| ); | ||
| return dropdownElement; | ||
| } | ||
|
|
||
| async selectElementWithDataKey(key: string): Promise<Locator> { | ||
| const element = this.frame.locator(`[data-key="${key}"]`); | ||
| return element; | ||
| } | ||
|
|
||
| async getInteractionAction(actionName: string): Promise<Locator> { | ||
| const interactionAction = await this.selectElementWithDataKey(actionName); | ||
| return interactionAction; | ||
| } | ||
|
|
||
| async getStateVariable(stateVar: string): Promise<Locator> { | ||
| const stateVariable = this.frame | ||
| .locator(`[data-test-id="0-${stateVar}"]`) | ||
| .first(); | ||
| return stateVariable; | ||
| } | ||
|
|
||
| async switchArena(name: string) { | ||
| await this.projectNavButton.click({ force: true }); | ||
|
||
| if (await this.projectNavClearSearchButton.isVisible()) { | ||
| await this.projectNavClearSearchButton.click({ force: true }); | ||
| } | ||
| await this.projectNavSearchInput.fill(name); | ||
| await this.frame.locator(`text=${name}`).click({ force: true }); | ||
| await this.page.waitForTimeout(1000); | ||
|
||
| } | ||
|
|
||
| async addInteraction(eventHandler: string, interaction: Interaction) { | ||
| await this.addInteractionButton.click(); | ||
| await this.interactionsSearchInput.fill(eventHandler); | ||
| const interactionsEventDropdownElement = | ||
| await this.selectInteractionEventById(eventHandler); | ||
| await interactionsEventDropdownElement.scrollIntoViewIfNeeded(); | ||
| await interactionsEventDropdownElement.click(); | ||
|
|
||
| await this.actionsDropdownButton.first().click(); | ||
| await (await this.getInteractionAction(interaction.actionName)).click(); | ||
| if (interaction.args.variable) { | ||
| await this.stateButton.click(); | ||
| await (await this.getStateVariable(interaction.args.variable[0])).click(); | ||
| await this.windowSaveButton.click(); | ||
| } | ||
| if (interaction.args.operation) { | ||
| await this.operationDropdownButton.click(); | ||
| await this.frame | ||
| .locator(`[data-key="${interaction.args.operation}"]`) | ||
| .click(); | ||
| } | ||
| if (interaction.args.value) { | ||
| await this.valueButton.click(); | ||
| await this.valueCodeInput.pressSequentially(interaction.args.value); | ||
| await this.windowSaveButton.click(); | ||
| } | ||
|
|
||
| await this.closeSidebarButton.click(); | ||
| } | ||
|
||
|
|
||
| async withinLiveMode(fn: (liveFrame: FrameLocator) => Promise<void>) { | ||
| await this.enterLiveModeButton.click({ force: true }); | ||
| await fn(this.liveFrame); | ||
| await this.exitLiveModeButton.click({ force: true }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { defineConfig, devices } from "@playwright/test"; | ||
| import { ENVIRONMENT, loadEnv } from "./types/environment"; | ||
|
|
||
| /** | ||
| * 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: "./e2e", | ||
| /* 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: ENVIRONMENT.baseUrl, | ||
|
|
||
| /* 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"] }, | ||
| }, | ||
|
|
||
| /* 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://localhost:3000', | ||
| // reuseExistingServer: !process.env.CI, | ||
| // }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { getEnvVar } from "../utils/env"; | ||
|
||
|
|
||
| export type Environment = { | ||
| baseUrl: string; | ||
| testUser: { | ||
| email: string; | ||
| password: string; | ||
| }; | ||
| }; | ||
|
|
||
| export function loadEnv(): Environment { | ||
|
||
| return { | ||
| baseUrl: getEnvVar("BASE_URL", "http://localhost:3003"), | ||
| testUser: { | ||
| email: getEnvVar("TEST_USER_EMAIL", "user2@example.com"), | ||
| password: getEnvVar("TEST_USER_PASSWORD", "!53kr3tz!"), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export const ENVIRONMENT = loadEnv(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of this pattern. It typically just obfuscates the actual values.
As a general rule, only make a variable for a constant if used 3 or more times.