diff --git a/.gitignore b/.gitignore index 5267ef1be..55ec3fee6 100644 --- a/.gitignore +++ b/.gitignore @@ -59,7 +59,8 @@ package-lock.json .nx/cache .nx/workspace-data -**/playwright/.auth/user.json +**/playwright/**/* +!**/playwright/.gitkeep # Ignore data directory in scripts /scripts/**/data/**/* diff --git a/apps/api/src/app/db/user.db.ts b/apps/api/src/app/db/user.db.ts index 525468694..9b33dfa7e 100644 --- a/apps/api/src/app/db/user.db.ts +++ b/apps/api/src/app/db/user.db.ts @@ -146,7 +146,7 @@ export const checkUserEntitlement = ({ userId: string; entitlement: keyof Omit; }): Promise => { - return prisma.entitlement.count({ where: { id: userId, [entitlement]: true } }).then((result) => result > 0); + return prisma.entitlement.count({ where: { userId, [entitlement]: true } }).then((result) => result > 0); }; export async function updateUser( diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index c15c09d55..2751f6eec 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -420,8 +420,16 @@ try { lastLoggedIn: new Date(), preferences: { create: { skipFrontdoorLogin: false } }, authFactors: { create: { type: '2fa-email', enabled: false } }, + entitlements: { create: { chromeExtension: true, recordSync: true, googleDrive: true } }, + }, + update: { + entitlements: { + upsert: { + create: { chromeExtension: true, recordSync: true, googleDrive: true }, + update: { chromeExtension: true, recordSync: true, googleDrive: true }, + }, + }, }, - update: {}, where: { id: user.id }, }); logger.info('Example user created'); diff --git a/apps/jetstream-e2e/project.json b/apps/jetstream-e2e/project.json index 1b8bc304f..a5cdd5d96 100644 --- a/apps/jetstream-e2e/project.json +++ b/apps/jetstream-e2e/project.json @@ -4,6 +4,7 @@ "sourceRoot": "apps/jetstream-e2e/src", "projectType": "application", "implicitDependencies": ["jetstream"], + "tags": ["scope:e2e"], "targets": { "e2e": { "executor": "@nx/playwright:playwright", diff --git a/apps/jetstream-e2e/src/fixtures/fixtures.ts b/apps/jetstream-e2e/src/fixtures/fixtures.ts index f5299aeb7..ca23d0080 100644 --- a/apps/jetstream-e2e/src/fixtures/fixtures.ts +++ b/apps/jetstream-e2e/src/fixtures/fixtures.ts @@ -1,15 +1,17 @@ import * as dotenv from 'dotenv'; +import { + ApiRequestUtils, + AuthenticationPage, + LoadSingleObjectPage, + LoadWithoutFilePage, + OrganizationsPage, + PlatformEventPage, + PlaywrightPage, + QueryPage, +} from '@jetstream/test/e2e-utils'; import { test as base } from '@playwright/test'; import { z } from 'zod'; -import { AuthenticationPage } from '../pageObjectModels/AuthenticationPage.model'; -import { LoadSingleObjectPage } from '../pageObjectModels/LoadSingleObjectPage.model'; -import { LoadWithoutFilePage } from '../pageObjectModels/LoadWithoutFilePage.model'; -import { OrganizationsPage } from '../pageObjectModels/OrganizationsPage'; -import { PlatformEventPage } from '../pageObjectModels/PlatformEventPage.model'; -import { PlaywrightPage } from '../pageObjectModels/PlaywrightPage.model'; -import { QueryPage } from '../pageObjectModels/QueryPage.model'; -import { ApiRequestUtils } from './ApiRequestUtils'; globalThis.__IS_CHROME_EXTENSION__ = false; diff --git a/apps/jetstream-e2e/src/setup/global.setup.ts b/apps/jetstream-e2e/src/setup/global.setup.ts index 35c5737a8..3660911cc 100644 --- a/apps/jetstream-e2e/src/setup/global.setup.ts +++ b/apps/jetstream-e2e/src/setup/global.setup.ts @@ -1,3 +1,4 @@ +/* eslint-disable playwright/no-standalone-expect */ import { ENV } from '@jetstream/api-config'; import { expect, test as setup } from '@playwright/test'; import { join } from 'path'; @@ -27,7 +28,6 @@ setup('login and ensure org exists', async ({ page, request }) => { await page.waitForURL(`${baseApiURL}/app`); - // eslint-disable-next-line playwright/no-standalone-expect await expect(page.getByRole('button', { name: 'Avatar' })).toBeVisible(); await page.evaluate(async () => { diff --git a/apps/jetstream-e2e/src/tests/authentication/login1.spec.ts b/apps/jetstream-e2e/src/tests/authentication/login1.spec.ts index dcdcb3e9c..6c1f9f5a9 100644 --- a/apps/jetstream-e2e/src/tests/authentication/login1.spec.ts +++ b/apps/jetstream-e2e/src/tests/authentication/login1.spec.ts @@ -20,7 +20,7 @@ test.describe('Login 1', () => { await test.step('Login and verify email and logout', async () => { await authenticationPage.loginAndVerifyEmail(email, password); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); await playwrightPage.goToProfile(); await expect(page.getByText(name)).toBeVisible(); await expect(page.getByText(email)).toBeVisible(); @@ -40,7 +40,7 @@ test.describe('Login 1', () => { await test.step('Login without MFA', async () => { await authenticationPage.fillOutLoginForm(email, password); await page.waitForURL(`**/app`); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); }); }); @@ -54,7 +54,7 @@ test.describe('Login 1', () => { await test.step('Login with remembered device', async () => { await authenticationPage.loginAndVerifyEmail(email, password, true); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); await playwrightPage.logout(); await expect(page.getByTestId('home-hero-container')).toBeVisible(); }); @@ -62,7 +62,7 @@ test.describe('Login 1', () => { await test.step('Should not need 2fa since device is remembered', async () => { await authenticationPage.fillOutLoginForm(email, password); await page.waitForURL(`**/app`); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); await playwrightPage.logout(); await expect(page.getByTestId('home-hero-container')).toBeVisible(); }); @@ -70,7 +70,7 @@ test.describe('Login 1', () => { await test.step('Email address should be case-insensitive', async () => { await authenticationPage.fillOutLoginForm(email.toUpperCase(), password); await page.waitForURL(`**/app`); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); }); }); }); diff --git a/apps/jetstream-e2e/src/tests/authentication/login2.spec.ts b/apps/jetstream-e2e/src/tests/authentication/login2.spec.ts index 3ed775ddd..212a74334 100644 --- a/apps/jetstream-e2e/src/tests/authentication/login2.spec.ts +++ b/apps/jetstream-e2e/src/tests/authentication/login2.spec.ts @@ -1,6 +1,6 @@ import { prisma } from '@jetstream/api-config'; +import { getPasswordResetToken } from '@jetstream/test/e2e-utils'; import { expect, test } from '../../fixtures/fixtures'; -import { getPasswordResetToken } from '../../utils/database-validation.utils'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -74,7 +74,7 @@ test.describe('Login 2', () => { await playwrightPage.logout(); await authenticationPage.loginAndVerifyTotp(email, password, secret); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); await playwrightPage.goToProfile(); @@ -95,7 +95,7 @@ test.describe('Login 2', () => { // Should not need 2fa since device is remembered await authenticationPage.fillOutLoginForm(email, password); await page.waitForURL(`**/app`); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); // re-enable TOTP to make sure that works await playwrightPage.goToProfile(); @@ -107,7 +107,7 @@ test.describe('Login 2', () => { // Ensure 2fa is reactivated on logout and login await authenticationPage.loginAndVerifyTotp(email, password, secret); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); // Delete TOTP and ensure that logout/login works successfully await playwrightPage.goToProfile(); @@ -121,6 +121,6 @@ test.describe('Login 2', () => { await authenticationPage.fillOutLoginForm(email, password); await page.waitForURL(`**/app`); - await expect(page.url()).toContain('/app'); + expect(page.url()).toContain('/app'); }); }); diff --git a/apps/jetstream-e2e/src/tests/authentication/login3.spec.ts b/apps/jetstream-e2e/src/tests/authentication/login3.spec.ts index c91c17f0b..c89eafc5d 100644 --- a/apps/jetstream-e2e/src/tests/authentication/login3.spec.ts +++ b/apps/jetstream-e2e/src/tests/authentication/login3.spec.ts @@ -38,13 +38,13 @@ test.describe('Login 3', () => { await test.step('Ensure authenticated API fails prior to email verification', async () => { const response = await apiRequestUtils.makeRequestRaw('GET', '/api/me', { Accept: 'application/json' }); - await expect(response.status()).toBe(401); + expect(response.status()).toBe(401); }); await test.step('Verify email and ensure we can make an authenticated API request', async () => { await authenticationPage.verifyEmail(email); const response = await apiRequestUtils.makeRequestRaw('GET', '/api/me', { Accept: 'application/json' }); - await expect(response.status()).toBe(200); + expect(response.status()).toBe(200); }); }); diff --git a/apps/jetstream-e2e/src/tests/load/load.spec.ts b/apps/jetstream-e2e/src/tests/load/load.spec.ts index a4424704b..bab1aeaa1 100644 --- a/apps/jetstream-e2e/src/tests/load/load.spec.ts +++ b/apps/jetstream-e2e/src/tests/load/load.spec.ts @@ -9,6 +9,7 @@ test.beforeEach(async ({ page }) => { test.describe.configure({ mode: 'parallel' }); test.describe('LOAD RECORDS', () => { + // eslint-disable-next-line playwright/expect-expect test('Should upload file', async ({ loadSingleObjectPage, page }) => { const csvFile = join(__dirname, `../../assets/records-Product2.csv`); diff --git a/apps/jetstream-e2e/src/tests/query/query-builder.spec.ts b/apps/jetstream-e2e/src/tests/query/query-builder.spec.ts index d95405d48..d77e5d449 100644 --- a/apps/jetstream-e2e/src/tests/query/query-builder.spec.ts +++ b/apps/jetstream-e2e/src/tests/query/query-builder.spec.ts @@ -8,6 +8,7 @@ test.describe.configure({ mode: 'parallel' }); test.describe('QUERY BUILDER', () => { // TODO: add test for drilling in to related query filters + // eslint-disable-next-line playwright/expect-expect test('should work with filters', async ({ queryPage }) => { await queryPage.goto(); await queryPage.selectObject('Account'); diff --git a/apps/jetstream-e2e/src/tests/security/security.spec.ts b/apps/jetstream-e2e/src/tests/security/security.spec.ts index f3ed52374..077bedd54 100644 --- a/apps/jetstream-e2e/src/tests/security/security.spec.ts +++ b/apps/jetstream-e2e/src/tests/security/security.spec.ts @@ -73,7 +73,7 @@ test.describe('Security Checks', () => { await test.step('Try to update or delete org from different user', async () => { const orgsResponse = await apiRequestUtils.makeRequestRaw('GET', `/api/orgs`); - await expect(orgsResponse.ok()).toEqual(true); + expect(orgsResponse.ok()).toEqual(true); const updateSalesforceOrgResponse = await apiRequestUtils.makeRequestRaw('PATCH', `/api/orgs/${salesforceOrg.uniqueId}`, { jetstreamOrganizationId: salesforceOrg.jetstreamOrganizationId, @@ -102,12 +102,12 @@ test.describe('Security Checks', () => { createdAt: salesforceOrg.createdAt, updatedAt: salesforceOrg.updatedAt, }); - await expect(updateSalesforceOrgResponse.ok()).toBeFalsy(); - await expect(updateSalesforceOrgResponse.status()).toBe(404); + expect(updateSalesforceOrgResponse.ok()).toBeFalsy(); + expect(updateSalesforceOrgResponse.status()).toBe(404); const deleteSalesforceOrgResponse = await apiRequestUtils.makeRequestRaw('DELETE', `/api/orgs/${salesforceOrg.uniqueId}`); - await expect(deleteSalesforceOrgResponse.ok()).toBeFalsy(); - await expect(deleteSalesforceOrgResponse.status()).toBe(404); + expect(deleteSalesforceOrgResponse.ok()).toBeFalsy(); + expect(deleteSalesforceOrgResponse.status()).toBe(404); }); await test.step('Try to use an org from a different user for a query API request', async () => { @@ -119,8 +119,8 @@ test.describe('Security Checks', () => { 'X-SFDC-ID': salesforceOrg.uniqueId, } ); - await expect(useOrgFromDifferentUserResponse.ok()).toBeFalsy(); - await expect(useOrgFromDifferentUserResponse.status()).toBe(404); + expect(useOrgFromDifferentUserResponse.ok()).toBeFalsy(); + expect(useOrgFromDifferentUserResponse.status()).toBe(404); }); await test.step('Try to update and delete a Jetstream organization from a different user', async () => { @@ -132,12 +132,12 @@ test.describe('Security Checks', () => { createdAt: jetstreamOrg.createdAt, updatedAt: jetstreamOrg.updatedAt, }); - await expect(updateJetstreamResponse.ok()).toBeFalsy(); - await expect(updateJetstreamResponse.status()).toBe(404); + expect(updateJetstreamResponse.ok()).toBeFalsy(); + expect(updateJetstreamResponse.status()).toBe(404); const deleteJetstreamResponse = await apiRequestUtils.makeRequestRaw('DELETE', `/api/orgs/${jetstreamOrg.id}`); - await expect(deleteJetstreamResponse.ok()).toBeFalsy(); - await expect(deleteJetstreamResponse.status()).toBe(404); + expect(deleteJetstreamResponse.ok()).toBeFalsy(); + expect(deleteJetstreamResponse.status()).toBe(404); }); }); }); diff --git a/apps/jetstream-web-extension-e2e/eslint.config.js b/apps/jetstream-web-extension-e2e/eslint.config.js index a7e50ea4d..f48bd1de4 100644 --- a/apps/jetstream-web-extension-e2e/eslint.config.js +++ b/apps/jetstream-web-extension-e2e/eslint.config.js @@ -12,8 +12,9 @@ module.exports = [ ...compat.extends('plugin:playwright/recommended'), { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, + rules: { + '@typescript-eslint/no-unused-vars': 'off', + }, }, { files: ['**/*.ts', '**/*.tsx'], diff --git a/apps/jetstream-web-extension-e2e/playwright.config.ts b/apps/jetstream-web-extension-e2e/playwright.config.ts index cdbff754d..c81b5d677 100644 --- a/apps/jetstream-web-extension-e2e/playwright.config.ts +++ b/apps/jetstream-web-extension-e2e/playwright.config.ts @@ -1,69 +1,59 @@ -import { defineConfig, devices } from '@playwright/test'; import { nxE2EPreset } from '@nx/playwright/preset'; +import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config(); -import { workspaceRoot } from '@nx/devkit'; +const ONE_SECOND = 1000; +const THIRTY_SECONDS = 30 * ONE_SECOND; // For CI, you may want to set BASE_URL to the deployed application. -const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; +const baseURL = process.env.NX_PUBLIC_SERVER_URL || 'http://localhost:3333'; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); +// Ensure tests run via VSCode debugger are run from the root of the repo +if (process.cwd().endsWith('/apps/jetstream-e2e')) { + process.chdir('../../'); +} /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ ...nxE2EPreset(__filename, { testDir: './src' }), - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + actionTimeout: THIRTY_SECONDS, + navigationTimeout: THIRTY_SECONDS, baseURL, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + permissions: ['notifications'], + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', }, /* Run your local dev server before starting the tests */ - webServer: { - command: 'yarn nx serve jetstream-web-extension', - url: 'http://localhost:4200', - reuseExistingServer: !process.env.CI, - cwd: workspaceRoot, - }, + // webServer: { + // command: 'yarn nx serve jetstream-web-extension', + // url: 'http://localhost:4200', + // reuseExistingServer: !process.env.CI, + // cwd: workspaceRoot, + // }, projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: 'setup', + testMatch: /.*\.setup\.ts/, + use: { + ...devices['Desktop Chrome'], + }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - // Uncomment for mobile browsers support - /* { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, - }, */ - - // Uncomment for branded browsers - /* { - name: 'Microsoft Edge', - use: { ...devices['Desktop Edge'], channel: 'msedge' }, + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + // Setup is giving all sorts of issues - login state is not being saved consistently + // storageState: 'playwright/.auth/web-ext-user.json', + }, + testMatch: /.*\.spec\.ts/, + // dependencies: ['setup'], }, - { - name: 'Google Chrome', - use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - } */ ], }); diff --git a/apps/jetstream-web-extension-e2e/project.json b/apps/jetstream-web-extension-e2e/project.json index 3be422535..2d9545adc 100644 --- a/apps/jetstream-web-extension-e2e/project.json +++ b/apps/jetstream-web-extension-e2e/project.json @@ -4,6 +4,7 @@ "projectType": "application", "sourceRoot": "apps/jetstream-web-extension-e2e/src", "implicitDependencies": ["jetstream-web-extension"], + "tags": ["scope:e2e"], "targets": { "e2e": { "executor": "@nx/playwright:playwright", diff --git a/apps/jetstream-web-extension-e2e/src/fixtures/fixtures.ts b/apps/jetstream-web-extension-e2e/src/fixtures/fixtures.ts new file mode 100644 index 000000000..55fb92d84 --- /dev/null +++ b/apps/jetstream-web-extension-e2e/src/fixtures/fixtures.ts @@ -0,0 +1,74 @@ +import * as dotenv from 'dotenv'; + +import { ApiRequestUtils, AuthenticationPage, WebExtensionPage } from '@jetstream/test/e2e-utils'; +import { workspaceRoot } from '@nx/devkit'; +import { test as base, chromium, Page, type BrowserContext } from '@playwright/test'; +import path from 'path'; +import { z } from 'zod'; + +globalThis.__IS_CHROME_EXTENSION__ = false; + +// Ensure tests run via VSCode debugger are run from the root of the repo +if (process.cwd().endsWith('/apps/jetstream-web-extension-e2e')) { + process.chdir('../../'); +} + +dotenv.config(); + +const environmentSchema = z.object({ + E2E_LOGIN_URL: z.string(), + E2E_LOGIN_USERNAME: z.string(), + E2E_LOGIN_PASSWORD: z.string(), +}); + +type Fixtures = { + environment: z.infer; + context: BrowserContext; + page: Page; + extensionId: string; + apiRequestUtils: ApiRequestUtils; + authenticationPage: AuthenticationPage; + webExtensionPage: WebExtensionPage; +}; + +export const test = base.extend({ + environment: async ({}, use) => { + await use(environmentSchema.parse(process.env)); + }, + context: async ({}, use) => { + const pathToExtension = path.join(workspaceRoot, 'dist/apps/jetstream-web-extension'); + const context = await chromium.launchPersistentContext('', { + // headless: false, + channel: 'chromium', + args: [`--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`], + }); + await use(context); + await context.close(); + }, + page: async ({ context }, use) => { + const page = await context.newPage(); + await use(page); + await page.close(); + }, + extensionId: async ({ context }, use) => { + // for manifest v3: + let [background] = context.serviceWorkers(); + if (!background) { + background = await context.waitForEvent('serviceworker'); + } + + const extensionId = background.url().split('/')[2]; + await use(extensionId); + }, + apiRequestUtils: async ({ page, environment }, use) => { + await use(new ApiRequestUtils(page, environment.E2E_LOGIN_USERNAME)); + }, + webExtensionPage: async ({ page, extensionId }, use) => { + await use(new WebExtensionPage(page, extensionId)); + }, + authenticationPage: async ({ page }, use) => { + await use(new AuthenticationPage(page)); + }, +}); + +export const expect = test.expect; diff --git a/apps/jetstream-web-extension-e2e/src/tests/auth.setup.ts b/apps/jetstream-web-extension-e2e/src/tests/auth.setup.ts new file mode 100644 index 000000000..caebfa58e --- /dev/null +++ b/apps/jetstream-web-extension-e2e/src/tests/auth.setup.ts @@ -0,0 +1,45 @@ +/* eslint-disable playwright/no-standalone-expect */ +import { ENV } from '@jetstream/api-config'; +import { join } from 'path'; +import { z } from 'zod'; +import { expect, test as setup } from '../fixtures/fixtures'; + +const environment = z + .object({ + E2E_LOGIN_URL: z.string(), + E2E_LOGIN_USERNAME: z.string(), + E2E_LOGIN_PASSWORD: z.string(), + }) + .parse(process.env); + +const baseApiURL = ENV.JETSTREAM_SERVER_URL!; +const baseAppURL = ENV.JETSTREAM_CLIENT_URL!; +const authFile = join('playwright/.auth/web-ext-user.json'); + +// FIXME: this does not seem to work - storageState is saved, but we are not logged in to Salesforce +setup('authenticate', async ({ page, context, extensionId, authenticationPage, webExtensionPage }) => { + console.log('GLOBAL SETUP - STARTED'); + + console.log('Logging in to Salesforce'); + await webExtensionPage.loginToSalesforce(environment.E2E_LOGIN_URL, environment.E2E_LOGIN_USERNAME, environment.E2E_LOGIN_PASSWORD); + await page.context().storageState({ path: authFile }); + + const alreadyLoggedIn = await page.getByRole('link', { name: 'Go to Jetstream' }).isVisible(); + + if (!alreadyLoggedIn) { + console.log('Logging in to Jetstream'); + const user = ENV.EXAMPLE_USER; + + await page.goto(baseApiURL); + + await authenticationPage.loginOrGoToAppIfLoggedIn(user.email, ENV.EXAMPLE_USER_PASSWORD as string); + + await expect(page.getByRole('button', { name: 'Avatar' })).toBeVisible(); + } + + console.log('GLOBAL SETUP - FINISHED\n'); + + console.log(`Saving storage state: ${authFile}\n`); + + context.storageState({ path: authFile }); +}); diff --git a/apps/jetstream-web-extension-e2e/src/tests/extension-test.spec.ts b/apps/jetstream-web-extension-e2e/src/tests/extension-test.spec.ts new file mode 100644 index 000000000..b0ec2dcc2 --- /dev/null +++ b/apps/jetstream-web-extension-e2e/src/tests/extension-test.spec.ts @@ -0,0 +1,127 @@ +import { ENV } from '@jetstream/api-config'; +import { QueryPage } from '@jetstream/test/e2e-utils'; +import { z } from 'zod'; +import { expect, test } from '../fixtures/fixtures'; + +const environment = z + .object({ + E2E_LOGIN_URL: z.string(), + E2E_LOGIN_USERNAME: z.string(), + E2E_LOGIN_PASSWORD: z.string(), + }) + .parse(process.env); + +test('Ensure we can login and logout of the extension', async ({ page, extensionId, webExtensionPage }) => { + const user = ENV.EXAMPLE_USER; + + await webExtensionPage.loginToJetstream(user.email, ENV.EXAMPLE_USER_PASSWORD as string); + await expect(page.getByText(/Logged in as/i)).toBeVisible(); + + await webExtensionPage.logout(); + await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible(); +}); + +test('Ensure extension shows up on Salesforce', async ({ page, webExtensionPage }) => { + const user = ENV.EXAMPLE_USER; + + await test.step('Login to Salesforce and Extension', async () => { + await webExtensionPage.loginToJetstream(user.email, ENV.EXAMPLE_USER_PASSWORD as string); + await expect(page.getByText(/Logged in as/i)).toBeVisible(); + + await webExtensionPage.loginToSalesforce(environment.E2E_LOGIN_URL, environment.E2E_LOGIN_USERNAME, environment.E2E_LOGIN_PASSWORD); + }); + + await test.step('Ensure page extension is visible in salesforce', async () => { + await expect(webExtensionPage.sfdcButton).toBeVisible(); + webExtensionPage.openPopup(); + + await expect(webExtensionPage.sfdcButtonPopupHeader).toBeVisible(); + await expect(webExtensionPage.sfdcButtonPopupBody).toBeVisible(); + + await webExtensionPage.closePopup(); + }); +}); + +test('Query page can be accessed and used', async ({ page, webExtensionPage, apiRequestUtils }) => { + const user = ENV.EXAMPLE_USER; + + await test.step('Login to Salesforce and Extension', async () => { + await webExtensionPage.loginToJetstream(user.email, ENV.EXAMPLE_USER_PASSWORD as string); + await expect(page.getByText(/Logged in as/i)).toBeVisible(); + + await webExtensionPage.loginToSalesforce(environment.E2E_LOGIN_URL, environment.E2E_LOGIN_USERNAME, environment.E2E_LOGIN_PASSWORD); + }); + + await test.step('Ensure page extension is visible in salesforce', async () => { + await expect(webExtensionPage.sfdcButton).toBeVisible(); + }); + + await test.step('Execute SOQL query', async () => { + const extensionPagePromise = page.waitForEvent('popup'); + + await webExtensionPage.goToAction('query'); + const extensionPage = await extensionPagePromise; + + const queryPage = new QueryPage(extensionPage, apiRequestUtils); + + await queryPage.selectObject('Account'); + await queryPage.selectFields(['Account Name', 'Account Description', 'Owner ID']); + await queryPage.executeBtn.click(); + await expect(extensionPage.getByText(/Showing [0-9,]+ of [0-9,]+ records/)).toBeVisible(); + }); +}); + +test('Record actions are available', async ({ page, webExtensionPage }) => { + const user = ENV.EXAMPLE_USER; + + await test.step('Login to Salesforce and Extension', async () => { + await webExtensionPage.loginToJetstream(user.email, ENV.EXAMPLE_USER_PASSWORD as string); + await expect(page.getByText(/Logged in as/i)).toBeVisible(); + + await webExtensionPage.loginToSalesforce(environment.E2E_LOGIN_URL, environment.E2E_LOGIN_USERNAME, environment.E2E_LOGIN_PASSWORD); + }); + + await test.step('Ensure page extension is visible in salesforce', async () => { + await expect(webExtensionPage.sfdcButton).toBeVisible(); + }); + + await test.step('Go to salesforce record page', async () => { + await page.getByRole('button', { name: 'App Launcher' }).click(); + await page.getByPlaceholder('Search apps and items...').fill('accounts'); + await page.getByRole('option', { name: 'Accounts' }).click(); + + await page.getByLabel('Search', { exact: true }).click(); + await page.getByPlaceholder('Search...').fill('burlington'); + await page.getByRole('dialog').getByTitle('Burlington Textiles Corp of').last().click(); + + await page.waitForURL((url) => url.pathname.includes('/Account/001')); + }); + + await test.step('View record in Jetstream', async () => { + await expect(webExtensionPage.sfdcButton).toBeVisible(); + webExtensionPage.openPopup(); + + await expect(webExtensionPage.sfdcButtonPopupRecordAction).toBeVisible(); + + const extensionPagePromise = page.waitForEvent('popup'); + await webExtensionPage.goToAction('viewRecord'); + const extensionPage = await extensionPagePromise; + + await expect(extensionPage.getByText(/View Record/)).toBeVisible(); + await expect(extensionPage.getByText(/Account - Burlington Textiles Corp of America/)).toBeVisible(); + }); + + await test.step('Edit record in Jetstream', async () => { + await expect(webExtensionPage.sfdcButton).toBeVisible(); + webExtensionPage.openPopup(); + + await expect(webExtensionPage.sfdcButtonPopupRecordAction).toBeVisible(); + + const extensionPagePromise = page.waitForEvent('popup'); + await webExtensionPage.goToAction('editRecord'); + const extensionPage = await extensionPagePromise; + + await expect(extensionPage.getByText(/Edit Record/)).toBeVisible(); + await expect(extensionPage.getByText(/Account - Burlington Textiles Corp of America/)).toBeVisible(); + }); +}); diff --git a/apps/jetstream-web-extension-e2e/tsconfig.json b/apps/jetstream-web-extension-e2e/tsconfig.json index 114364a11..ead0fcb01 100644 --- a/apps/jetstream-web-extension-e2e/tsconfig.json +++ b/apps/jetstream-web-extension-e2e/tsconfig.json @@ -4,6 +4,7 @@ "allowJs": true, "outDir": "../../dist/out-tsc", "module": "commonjs", + "allowSyntheticDefaultImports": true, "sourceMap": false }, "include": [ diff --git a/apps/jetstream-web-extension/src/components/SfdcPageButton.tsx b/apps/jetstream-web-extension/src/components/SfdcPageButton.tsx index 91247ef07..f12cbde58 100644 --- a/apps/jetstream-web-extension/src/components/SfdcPageButton.tsx +++ b/apps/jetstream-web-extension/src/components/SfdcPageButton.tsx @@ -200,6 +200,7 @@ export function SfdcPageButton() { return ( <> - - ); -} diff --git a/apps/jetstream-web-extension/src/pages/options/options.html b/apps/jetstream-web-extension/src/pages/options/options.html deleted file mode 100644 index 997d6d0bb..000000000 --- a/apps/jetstream-web-extension/src/pages/options/options.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Jetstream - - -
- -
- - diff --git a/apps/jetstream-web-extension/src/pages/popup/Popup.tsx b/apps/jetstream-web-extension/src/pages/popup/Popup.tsx index 99fe5d15f..228c89626 100644 --- a/apps/jetstream-web-extension/src/pages/popup/Popup.tsx +++ b/apps/jetstream-web-extension/src/pages/popup/Popup.tsx @@ -83,14 +83,14 @@ export function Component() { )} {(!loggedIn || !authTokens) && ( <> -

To get started with Jetstream, login to your account.

+

To get started with Jetstream, sign in to your account.

- Login + Sign In )} diff --git a/apps/jetstream-web-extension/src/serviceWorker.ts b/apps/jetstream-web-extension/src/serviceWorker.ts index 22a34904e..6317fca77 100644 --- a/apps/jetstream-web-extension/src/serviceWorker.ts +++ b/apps/jetstream-web-extension/src/serviceWorker.ts @@ -409,7 +409,7 @@ async function handleLogout(sender: chrome.runtime.MessageSender): Promise { diff --git a/apps/jetstream-web-extension/src/utils/extension.store.ts b/apps/jetstream-web-extension/src/utils/extension.store.ts index 2711245f8..75db85b07 100644 --- a/apps/jetstream-web-extension/src/utils/extension.store.ts +++ b/apps/jetstream-web-extension/src/utils/extension.store.ts @@ -14,9 +14,16 @@ chrome.storage.onChanged.addListener((changes, namespace) => { ...prevValue.sync, }, }; + for (const [key, { newValue }] of Object.entries(changes)) { newState[namespace][key] = newValue; } + + newState.local.options = newState.local.options ?? { enabled: true }; + + newState.sync.authTokens = newState.sync.authTokens ?? null; + newState.sync.extIdentifier = newState.sync.extIdentifier ?? null; + return newState; }); } diff --git a/apps/jetstream-web-extension/webpack.config.js b/apps/jetstream-web-extension/webpack.config.js index 786cd4855..f03062492 100644 --- a/apps/jetstream-web-extension/webpack.config.js +++ b/apps/jetstream-web-extension/webpack.config.js @@ -13,7 +13,6 @@ module.exports = composePlugins(withNx(), withReact(), (config) => { config.entry = { app: './src/pages/app/App.tsx', popup: './src/pages/popup/Popup.tsx', - options: './src/pages/options/Options.tsx', serviceWorker: './src/serviceWorker.ts', contentScript: './src/contentScript.tsx', }; @@ -44,7 +43,6 @@ module.exports = composePlugins(withNx(), withReact(), (config) => { }), createHtmlPagePlugin('app'), createHtmlPagePlugin('popup'), - createHtmlPagePlugin('options'), createHtmlPlaceholderPagePlugin('home'), createHtmlPlaceholderPagePlugin('organizations'), diff --git a/eslint.config.js b/eslint.config.js index eb305b4be..51a314879 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,10 @@ module.exports = [ sourceTag: 'scope:server', onlyDependOnLibsWithTags: ['scope:server', 'scope:type-only', 'scope:shared', 'scope:any'], }, + { + sourceTag: 'scope:e2e', + onlyDependOnLibsWithTags: ['scope:server', 'scope:type-only', 'scope:shared', 'scope:e2e', 'scope:any'], + }, { sourceTag: 'scope:worker', onlyDependOnLibsWithTags: ['scope:allow-worker-import', 'scope:type-only', 'scope:shared', 'scope:any'], diff --git a/libs/test/e2e-utils/eslint.config.cjs b/libs/test/e2e-utils/eslint.config.cjs new file mode 100644 index 000000000..07e518f72 --- /dev/null +++ b/libs/test/e2e-utils/eslint.config.cjs @@ -0,0 +1,3 @@ +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [...baseConfig]; diff --git a/libs/test/e2e-utils/project.json b/libs/test/e2e-utils/project.json new file mode 100644 index 000000000..16edd861e --- /dev/null +++ b/libs/test/e2e-utils/project.json @@ -0,0 +1,9 @@ +{ + "name": "e2e-utils", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/test/e2e-utils/src", + "projectType": "library", + "tags": ["scope:e2e"], + "// targets": "to see all targets run: nx show project e2e-utils --web", + "targets": {} +} diff --git a/libs/test/e2e-utils/src/index.ts b/libs/test/e2e-utils/src/index.ts new file mode 100644 index 000000000..885ecc58f --- /dev/null +++ b/libs/test/e2e-utils/src/index.ts @@ -0,0 +1,10 @@ +export * from './lib/ApiRequestUtils'; +export * from './lib/e2e-database-validation.utils'; +export * from './lib/pageObjectModels/AuthenticationPage.model'; +export * from './lib/pageObjectModels/LoadSingleObjectPage.model'; +export * from './lib/pageObjectModels/LoadWithoutFilePage.model'; +export * from './lib/pageObjectModels/OrganizationsPage'; +export * from './lib/pageObjectModels/PlatformEventPage.model'; +export * from './lib/pageObjectModels/PlaywrightPage.model'; +export * from './lib/pageObjectModels/QueryPage.model'; +export * from './lib/pageObjectModels/WebExtensionPage.model'; diff --git a/apps/jetstream-e2e/src/fixtures/ApiRequestUtils.ts b/libs/test/e2e-utils/src/lib/ApiRequestUtils.ts similarity index 100% rename from apps/jetstream-e2e/src/fixtures/ApiRequestUtils.ts rename to libs/test/e2e-utils/src/lib/ApiRequestUtils.ts diff --git a/apps/jetstream-e2e/src/utils/database-validation.utils.ts b/libs/test/e2e-utils/src/lib/e2e-database-validation.utils.ts similarity index 100% rename from apps/jetstream-e2e/src/utils/database-validation.utils.ts rename to libs/test/e2e-utils/src/lib/e2e-database-validation.utils.ts diff --git a/apps/jetstream-e2e/src/pageObjectModels/AuthenticationPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/AuthenticationPage.model.ts similarity index 96% rename from apps/jetstream-e2e/src/pageObjectModels/AuthenticationPage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/AuthenticationPage.model.ts index 11e3e1675..93316fec0 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/AuthenticationPage.model.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/AuthenticationPage.model.ts @@ -6,7 +6,7 @@ import { getUserSessionByEmail, hasPasswordResetToken, verifyEmailLogEntryExists, -} from '../utils/database-validation.utils'; +} from '../e2e-database-validation.utils'; export class AuthenticationPage { readonly page: Page; @@ -340,4 +340,14 @@ export class AuthenticationPage { await this.submitButton.click(); } + + async loginOrGoToAppIfLoggedIn(email: string, password: string) { + const alreadyLoggedInBtn = this.page.getByRole('link', { name: 'Go to Jetstream' }); + + if (await alreadyLoggedInBtn.isVisible()) { + await alreadyLoggedInBtn.click(); + } else { + await this.fillOutLoginForm(email, password); + } + } } diff --git a/apps/jetstream-e2e/src/pageObjectModels/LoadSingleObjectPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/LoadSingleObjectPage.model.ts similarity index 98% rename from apps/jetstream-e2e/src/pageObjectModels/LoadSingleObjectPage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/LoadSingleObjectPage.model.ts index 239760905..dc2464617 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/LoadSingleObjectPage.model.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/LoadSingleObjectPage.model.ts @@ -1,5 +1,5 @@ import { expect, Locator, Page } from '@playwright/test'; -import { ApiRequestUtils } from '../fixtures/ApiRequestUtils'; +import { ApiRequestUtils } from '../ApiRequestUtils'; import { PlaywrightPage } from './PlaywrightPage.model'; export class LoadSingleObjectPage { diff --git a/apps/jetstream-e2e/src/pageObjectModels/LoadWithoutFilePage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/LoadWithoutFilePage.model.ts similarity index 97% rename from apps/jetstream-e2e/src/pageObjectModels/LoadWithoutFilePage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/LoadWithoutFilePage.model.ts index e2e079c2c..ffe93cb4d 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/LoadWithoutFilePage.model.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/LoadWithoutFilePage.model.ts @@ -1,5 +1,5 @@ import { Locator, Page } from '@playwright/test'; -import { ApiRequestUtils } from '../fixtures/ApiRequestUtils'; +import { ApiRequestUtils } from '../ApiRequestUtils'; import { PlaywrightPage } from './PlaywrightPage.model'; export class LoadWithoutFilePage { diff --git a/apps/jetstream-e2e/src/pageObjectModels/OrganizationsPage.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/OrganizationsPage.ts similarity index 97% rename from apps/jetstream-e2e/src/pageObjectModels/OrganizationsPage.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/OrganizationsPage.ts index fbe31dbc0..e6fad6806 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/OrganizationsPage.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/OrganizationsPage.ts @@ -1,5 +1,5 @@ import { APIRequestContext, Locator, Page, expect } from '@playwright/test'; -import { ApiRequestUtils } from '../fixtures/ApiRequestUtils'; +import { ApiRequestUtils } from '../ApiRequestUtils'; import { PlaywrightPage } from './PlaywrightPage.model'; export class OrganizationsPage { @@ -62,7 +62,6 @@ export class OrganizationsPage { const salesforcePage = await salesforcePagePromise; // Sometimes SFDC clears the values from the form if they are typed in too quickly - // eslint-disable-next-line playwright/no-wait-for-timeout await salesforcePage.waitForTimeout(1000); await salesforcePage.getByLabel('Username').click(); diff --git a/apps/jetstream-e2e/src/pageObjectModels/PlatformEventPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/PlatformEventPage.model.ts similarity index 97% rename from apps/jetstream-e2e/src/pageObjectModels/PlatformEventPage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/PlatformEventPage.model.ts index 04960a8c7..0be5a4989 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/PlatformEventPage.model.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/PlatformEventPage.model.ts @@ -1,5 +1,5 @@ import { Locator, Page, expect } from '@playwright/test'; -import { ApiRequestUtils } from '../fixtures/ApiRequestUtils'; +import { ApiRequestUtils } from '../ApiRequestUtils'; import { PlaywrightPage } from './PlaywrightPage.model'; export class PlatformEventPage { diff --git a/apps/jetstream-e2e/src/pageObjectModels/PlaywrightPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/PlaywrightPage.model.ts similarity index 100% rename from apps/jetstream-e2e/src/pageObjectModels/PlaywrightPage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/PlaywrightPage.model.ts diff --git a/apps/jetstream-e2e/src/pageObjectModels/QueryPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/QueryPage.model.ts similarity index 96% rename from apps/jetstream-e2e/src/pageObjectModels/QueryPage.model.ts rename to libs/test/e2e-utils/src/lib/pageObjectModels/QueryPage.model.ts index 16928d60a..3f8e93d79 100644 --- a/apps/jetstream-e2e/src/pageObjectModels/QueryPage.model.ts +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/QueryPage.model.ts @@ -3,21 +3,18 @@ import { isRecordWithId } from '@jetstream/shared/utils'; import { QueryFilterOperator, QueryResults } from '@jetstream/types'; import { Locator, Page, expect } from '@playwright/test'; import { isNumber } from 'lodash'; -import { ApiRequestUtils } from '../fixtures/ApiRequestUtils'; -import { PlaywrightPage } from './PlaywrightPage.model'; +import { ApiRequestUtils } from '../ApiRequestUtils'; export class QueryPage { readonly apiRequestUtils: ApiRequestUtils; - readonly playwrightPage: PlaywrightPage; readonly page: Page; readonly sobjectList: Locator; readonly fieldsList: Locator; readonly soqlQuery: Locator; readonly executeBtn: Locator; - constructor(page: Page, apiRequestUtils: ApiRequestUtils, playwrightPage: PlaywrightPage) { + constructor(page: Page, apiRequestUtils: ApiRequestUtils) { this.apiRequestUtils = apiRequestUtils; - this.playwrightPage = playwrightPage; this.page = page; this.sobjectList = page.getByTestId('sobject-list'); this.fieldsList = page.getByTestId('sobject-fields'); @@ -43,7 +40,6 @@ export class QueryPage { if (action === 'EXECUTE') { await Promise.all([ - // eslint-disable-next-line playwright/no-networkidle this.page.waitForURL('**/query/results', { waitUntil: 'networkidle' }), manualQueryPopover.getByRole('link', { name: 'Execute' }).click(), ]); diff --git a/libs/test/e2e-utils/src/lib/pageObjectModels/WebExtensionPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/WebExtensionPage.model.ts new file mode 100644 index 000000000..2f96a8a70 --- /dev/null +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/WebExtensionPage.model.ts @@ -0,0 +1,91 @@ +import { Locator, Page, expect } from '@playwright/test'; +import { AuthenticationPage } from './AuthenticationPage.model'; + +const ButtonAction = { + query: 'Query Records', + load: 'Load Records', + automationControl: 'Automation Control', + managePermissions: 'Manage Permissions', + deploy: 'Deploy and View Metadata', + apex: 'Anonymous Apex', + editRecord: 'Edit Current Record', + viewRecord: 'View Current Record', +}; + +export class WebExtensionPage { + readonly page: Page; + readonly extensionId: string; + + readonly sfdcButton: Locator; + readonly sfdcButtonPopupHeader: Locator; + readonly sfdcButtonPopupBody: Locator; + readonly sfdcButtonPopupRecordAction: Locator; + readonly sfdcAppLauncherBtn: Locator; + + constructor(page: Page, extensionId: string) { + this.page = page; + this.extensionId = extensionId; + + this.sfdcButton = page.getByTestId('jetstream-ext-page-button'); + this.sfdcButtonPopupHeader = page.getByTestId('jetstream-ext-popup-header'); + this.sfdcButtonPopupBody = page.getByTestId('jetstream-ext-popup-body'); + this.sfdcButtonPopupRecordAction = page.getByTestId('jetstream-ext-popup-body').getByText('Record Actions'); + this.sfdcAppLauncherBtn = this.page.getByRole('button', { name: 'App Launcher' }); + } + + get baseExtensionUrl() { + return `chrome-extension://${this.extensionId}`; + } + + async gotoPopup() { + await this.page.goto(`${this.baseExtensionUrl}/popup.html`); + } + + async loginToSalesforce(url: string, username: string, password: string) { + await this.page.goto(url); + + await this.page.getByLabel('Username').click(); + await this.page.getByLabel('Username').fill(username); + + await this.page.getByLabel('Password').click(); + await this.page.getByLabel('Password').fill(password); + + await this.page.getByRole('button', { name: 'Log In' }).click(); + + await expect(this.page.getByRole('button', { name: 'App Launcher' })).toBeVisible(); + } + + async loginToJetstream(email: string, password: string) { + await this.gotoPopup(); + + const jetstreamExtAuthPagePromise = this.page.waitForEvent('popup'); + + await this.page.getByRole('link', { name: 'Sign in' }).click(); + + const jetstreamExtAuthPage = await jetstreamExtAuthPagePromise; + + const tempAuthPage = new AuthenticationPage(jetstreamExtAuthPage); + await tempAuthPage.fillOutLoginForm(email, password); + + await expect(jetstreamExtAuthPage.getByText(/successfully authenticated/i)).toBeVisible(); + await jetstreamExtAuthPage.close(); + } + + async logout() { + await this.gotoPopup(); + await this.page.getByRole('button', { name: 'Log Out' }).click(); + } + + async openPopup() { + this.sfdcButton.click(); + } + + async closePopup() { + await this.sfdcAppLauncherBtn.click(); + } + + async goToAction(action: keyof typeof ButtonAction) { + await this.sfdcButton.click(); + await this.page.getByRole('link', { name: ButtonAction[action] }).click(); + } +} diff --git a/libs/test/e2e-utils/tsconfig.json b/libs/test/e2e-utils/tsconfig.json new file mode 100644 index 000000000..9bde19595 --- /dev/null +++ b/libs/test/e2e-utils/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/test/e2e-utils/tsconfig.lib.json b/libs/test/e2e-utils/tsconfig.lib.json new file mode 100644 index 000000000..f554b127d --- /dev/null +++ b/libs/test/e2e-utils/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/package.json b/package.json index 1bdfebb70..4f231a214 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,8 @@ "playwright:test:with-server": "yarn start-server-and-test --expect 200 'yarn start:e2e' http://localhost:3333 'yarn playwright:test'", "playwright:test": "yarn playwright test src --config apps/jetstream-e2e/playwright.config.ts", "playwright:test:query-results": "yarn playwright test query-results.spec.ts --config apps/jetstream-e2e/playwright.config.ts --headed", + "playwright:web-extension:test": "yarn playwright test src --config apps/jetstream-web-extension-e2e/playwright.config.ts --headed", + "playwright:web-extension:open": "yarn playwright open http://localhost:3333 --config apps/jetstream-web-extension-e2e/playwright.config.ts", "format": "nx format:write", "format:write": "nx format:write", "format:check": "nx format:check", diff --git a/playwright/.gitkeep b/playwright/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tsconfig.base.json b/tsconfig.base.json index 17053d6b0..114faf2b5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -51,6 +51,7 @@ "@jetstream/shared/ui-utils": ["libs/shared/ui-utils/src/index.ts"], "@jetstream/shared/utils": ["libs/shared/utils/src/index.ts"], "@jetstream/splitjs": ["libs/splitjs/src/index.ts"], + "@jetstream/test/e2e-utils": ["libs/test/e2e-utils/src/index.ts"], "@jetstream/types": ["libs/types/src/index.ts"], "@jetstream/ui": ["libs/ui/src/index.ts"], "@jetstream/ui-core": ["libs/shared/ui-core/src/index.ts"],