diff --git a/.gitignore b/.gitignore index fb29016267..8f04b375c5 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,8 @@ src/plugins/* # Ignore all other lock files package-lock.json -yarn.lock \ No newline at end of file +yarn.lock +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/agent.test.ts b/e2e/agent.test.ts new file mode 100644 index 0000000000..25292a810c --- /dev/null +++ b/e2e/agent.test.ts @@ -0,0 +1,47 @@ +import { test, expect } from "@playwright/test"; +import { loginAsAdmin } from "./test-util"; +import { checkActiveTab, findColumnIndex } from "./test-util-antd"; + +test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.getByRole("menuitem", { name: "hdd Resources" }).click(); + await expect( + page.getByRole("heading", { name: "Computation Resources" }), + ).toBeVisible(); +}); + +test.describe("Agent list", () => { + let resourcesPageTab; + let agentListTable; + + test.beforeEach(async ({ page }) => { + const firstCard = await page + .locator(".ant-layout-content .ant-card") + .first(); + resourcesPageTab = await firstCard.locator(".ant-tabs"); + agentListTable = await firstCard.locator(".ant-table"); + }); + + test("should have at least one connected agent", async ({ page }) => { + const firstCard = await page + .locator(".ant-layout-content .ant-card") + .first(); + await checkActiveTab(firstCard.locator(".ant-tabs"), "Agent"); + + const selectedSegment = await page.locator(".ant-segmented-item-selected"); + await expect(selectedSegment).toContainText("Connected"); + + const rows = await agentListTable.locator(".ant-table-row"); + const rowCount = await rows.count(); + expect(rowCount).toBeGreaterThan(0); + const firstRow = rows.first(); + + const columnIndex = await findColumnIndex(agentListTable, "ID / Endpoint"); + const specificColumn = await firstRow + .locator(".ant-table-cell") + .nth(columnIndex); + const columnText = await specificColumn.textContent(); + const firstAgentId = columnText?.split("tcp://")[0]; + expect(firstAgentId).toBeTruthy(); + }); +}); diff --git a/e2e/login.test.ts b/e2e/login.test.ts new file mode 100644 index 0000000000..5afaac67b3 --- /dev/null +++ b/e2e/login.test.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { loginAsAdmin } from './test-util'; + +test.beforeEach(async ({ page }) => { + await page.goto('http://127.0.0.1:9081'); + +}); + +test.describe('Before login', () => { + test('should display the login form', async ({ page }) => { + await expect(page.getByLabel('E-mail or Username')).toBeVisible(); + await expect(page.locator('#id_password label')).toBeVisible(); + await expect(page.getByLabel('Login', { exact: true })).toBeVisible(); + }); +}); + +test.describe('Login using the admin account', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('should redirect to the Summary', async ({ page }) => { + await expect(page).toHaveURL(/\/summary/); + await expect(page.getByRole('heading', { name: 'Summary' })).toBeVisible(); + }); +}); + diff --git a/e2e/test-1.spec.ts b/e2e/test-1.spec.ts new file mode 100644 index 0000000000..5b3d48cfd1 --- /dev/null +++ b/e2e/test-1.spec.ts @@ -0,0 +1,5 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + // Recording... +}); \ No newline at end of file diff --git a/e2e/test-util-antd.ts b/e2e/test-util-antd.ts new file mode 100644 index 0000000000..0492c98d34 --- /dev/null +++ b/e2e/test-util-antd.ts @@ -0,0 +1,25 @@ +import { Locator, expect } from "@playwright/test"; + +export async function checkActiveTab( + tabsLocator: Locator, + expectedTabName: string, +) { + const activeTab = await tabsLocator.locator(".ant-tabs-tab-active"); + await expect(activeTab).toContainText(expectedTabName); +} + +export async function getTableHeaders(page: Locator) { + return await page.locator(".ant-table-thead th"); +} + +export async function findColumnIndex( + tableLocator: Locator, + columnTitle: string, +) { + const headers = await tableLocator.locator(".ant-table-thead th"); + const columnIndex = await headers.evaluateAll((ths, title) => { + return ths.findIndex((th) => th.textContent?.trim() === title); + }, columnTitle); + + return columnIndex; +} diff --git a/e2e/test-util.ts b/e2e/test-util.ts new file mode 100644 index 0000000000..090c3e39c0 --- /dev/null +++ b/e2e/test-util.ts @@ -0,0 +1,89 @@ +import { Page, expect } from "@playwright/test"; + +export async function login( + page: Page, + username: string, + password: string, + endpoint: string, +) { + await page.goto("http://127.0.0.1:9081"); + await page.locator("#id_password label").click(); + await page.getByLabel("E-mail or Username").click(); + await page.getByLabel("E-mail or Username").fill(username); + await page.getByRole("textbox", { name: "Password" }).click(); + await page.getByRole("textbox", { name: "Password" }).fill(password); + await page.getByRole("textbox", { name: "Endpoint" }).click(); + await page.getByRole("textbox", { name: "Endpoint" }).fill(endpoint); + await page.getByLabel("Login", { exact: true }).click(); + await page.waitForSelector('[data-testid="user-dropdown-button"]'); + +} + +export async function loginAsAdmin(page: Page) { + await login(page, "admin@lablup.com", "wJalrXUt", "http://127.0.0.1:8090"); +} +export async function loginAsDomainAdmin(page: Page) { + await login( + page, + "domain-admin@lablup.com", + "cWbsM_vB", + "http://127.0.0.1:8090", + ); +} +export async function loginAsUser(page: Page) { + await login(page, "user@lablup.com", "C8qnIo29", "http://127.0.0.1:8090"); +} +export async function loginAsUser2(page: Page) { + await login(page, "user2@lablup.com", "P7oxTDdz", "http://127.0.0.1:8090"); +} +export async function loginAsMonitor(page: Page) { + await login(page, "monitor@lablup.com", "7tuEwF1J", "http://127.0.0.1:8090"); +} + +export async function logout(page: Page) { + await page.locator("text=Logout").click(); + await page.getByTestId("user-dropdown-button").click(); + await page.getByText("Log Out").click(); +} + +export async function navigateTo(page: Page, path: string) { + //merge the base url with the path + const url = new URL(path, "http://127.0.0.1:8090"); + await page.goto(url.toString()); +} + +export async function createVFolderAndVerify(page: Page, folderName: string) { + await navigateTo(page, 'data'); + + await page.getByRole('button', { name: 'plus Add' }).click(); + // TODO: wait for initial rendering without timeout + await page.waitForTimeout(1000); + await page.getByRole('textbox', { name: 'Folder name*' }).click(); + await page.getByRole('textbox', { name: 'Folder name*' }).fill(folderName); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + await page.locator('#input-vaadin-text-field-18').click(); + await page.locator('#input-vaadin-text-field-18').fill(folderName); + await page.waitForSelector(`text=folder_open ${folderName}`); +} + +export async function deleteVFolderAndVerify(page: Page, folderName: string) { + await navigateTo(page, 'data'); + await page.locator('#input-vaadin-text-field-18').click(); + await page.locator('#input-vaadin-text-field-18').fill(folderName); + await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'delete' }).first().click(); + await page.locator('#delete-without-confirm-button').getByLabel('delete').click(); + await page.getByRole('tab', { name: 'delete' }).click(); + await page.locator('#input-vaadin-text-field-84').click(); + await page.locator('#input-vaadin-text-field-84').fill(folderName); + await page.getByLabel('delete_forever').click(); + await page.getByRole('textbox', { name: 'Type folder name to delete' }).fill(folderName); + await page.waitForTimeout(1000); + await page.getByRole('textbox', { name: 'Type folder name to delete' }).click(); + + await expect(page.locator('#trash-bin-folder-storage').getByText('e2e-test-folder', { exact: true })).toBeVisible(); + + await page.getByRole('button', { name: 'Delete forever' }).click(); + await expect(page.locator('#trash-bin-folder-storage').getByText('e2e-test-folder', { exact: true })).toBeHidden(); +} diff --git a/e2e/vfolder.test.ts b/e2e/vfolder.test.ts new file mode 100644 index 0000000000..d053395c86 --- /dev/null +++ b/e2e/vfolder.test.ts @@ -0,0 +1,11 @@ +import { test } from '@playwright/test'; +import { createVFolderAndVerify, deleteVFolderAndVerify, loginAsUser } from './test-util'; + + +test.describe('VFolder ', () => { + test('User can create and delete vFolder', async ({ page }) => { + await loginAsUser(page); + await createVFolderAndVerify(page, 'e2e-test-folder'); + await deleteVFolderAndVerify(page, 'e2e-test-folder'); + }); +}); \ No newline at end of file diff --git a/package.json b/package.json index b9e8d2416a..e6b46b5746 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@babel/preset-typescript": "^7.24.7", "@babel/types": "^7.25.2", "@electron/packager": "^18.3.3", + "@playwright/test": "^1.46.1", "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", @@ -123,6 +124,7 @@ "@types/estree": "1.0.5", "@types/hammerjs": "^2.0.45", "@types/jest": "^29.5.12", + "@types/node": "^22.4.1", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@web/dev-server": "^0.4.6", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..588e4e3f99 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// 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: 'http://127.0.0.1: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'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* 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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc901f95fe..4535b0fee6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,9 @@ importers: '@electron/packager': specifier: ^18.3.3 version: 18.3.3 + '@playwright/test': + specifier: ^1.46.1 + version: 1.46.1 '@rollup/plugin-commonjs': specifier: ^25.0.8 version: 25.0.8(rollup@4.20.0) @@ -261,6 +264,9 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.12 + '@types/node': + specifier: ^22.4.1 + version: 22.4.1 '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) @@ -338,7 +344,7 @@ importers: version: 4.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.1.0) + version: 29.7.0(@types/node@22.4.1) jsdoc: specifier: ^4.0.3 version: 4.0.3 @@ -380,7 +386,7 @@ importers: version: 4.0.0 ts-jest: specifier: ^29.2.4 - version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.1.0))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.4.1))(typescript@5.5.4) tslib: specifier: ^2.6.3 version: 2.6.3 @@ -1930,6 +1936,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.46.1': + resolution: {integrity: sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==} + engines: {node: '>=18'} + hasBin: true + '@polymer/polymer@3.5.1': resolution: {integrity: sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==} @@ -2266,8 +2277,8 @@ packages: '@types/node@20.14.14': resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} - '@types/node@22.1.0': - resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} + '@types/node@22.4.1': + resolution: {integrity: sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==} '@types/parse5@2.2.34': resolution: {integrity: sha512-p3qOvaRsRpFyEmaS36RtLzpdxZZnmxGuT1GMgzkTtTJVFuEw7KFjGK83MFODpJExgX1bEzy9r0NYjMC3IMfi7w==} @@ -4387,6 +4398,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6291,6 +6307,16 @@ packages: node-notifier: optional: true + playwright-core@1.46.1: + resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.46.1: + resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} + engines: {node: '>=18'} + hasBin: true + plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -7554,8 +7580,8 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.13.0: - resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} uni-global@1.0.0: resolution: {integrity: sha512-WWM3HP+siTxzIWPNUg7hZ4XO8clKi6NoCAJJWnuRL+BAqyFXF8gC03WNyTefGoUXYc47uYgXxpKLIEvo65PEHw==} @@ -9308,7 +9334,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -9321,14 +9347,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.1.0) + jest-config: 29.7.0(@types/node@22.4.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -9353,7 +9379,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -9371,7 +9397,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.1.0 + '@types/node': 22.4.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -9393,7 +9419,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -9463,7 +9489,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -10365,6 +10391,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.46.1': + dependencies: + playwright: 1.46.1 + '@polymer/polymer@3.5.1': dependencies: '@webcomponents/shadycss': 1.11.2 @@ -10563,7 +10593,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/aws-lambda@8.10.143': {} @@ -10597,7 +10627,7 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/btoa-lite@1.0.2': {} @@ -10605,14 +10635,14 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/responselike': 1.0.3 '@types/command-line-args@5.2.3': {} '@types/connect@3.4.38': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/content-disposition@0.5.8': {} @@ -10621,7 +10651,7 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 4.17.21 '@types/keygrip': 1.0.6 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/drawflow@0.0.11': {} @@ -10643,7 +10673,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -10657,17 +10687,17 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 optional: true '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/hammerjs@2.0.45': {} @@ -10696,13 +10726,13 @@ snapshots: '@types/jsonwebtoken@9.0.6': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/keygrip@1.0.6': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/koa-compose@3.2.8': dependencies: @@ -10717,7 +10747,7 @@ snapshots: '@types/http-errors': 2.0.4 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/linkify-it@5.0.0': {} @@ -10738,13 +10768,13 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@22.1.0': + '@types/node@22.4.1': dependencies: - undici-types: 6.13.0 + undici-types: 6.19.8 '@types/parse5@2.2.34': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/parse5@6.0.3': {} @@ -10756,17 +10786,17 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -10777,7 +10807,7 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 '@types/yargs-parser@21.0.3': {} @@ -10787,7 +10817,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 optional: true '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': @@ -12508,13 +12538,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - create-jest@29.7.0(@types/node@22.1.0): + create-jest@29.7.0(@types/node@22.4.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.1.0) + jest-config: 29.7.0(@types/node@22.4.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -13598,6 +13628,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -14377,7 +14410,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -14397,16 +14430,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.1.0): + jest-cli@29.7.0(@types/node@22.4.1): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.1.0) + create-jest: 29.7.0(@types/node@22.4.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.1.0) + jest-config: 29.7.0(@types/node@22.4.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -14416,7 +14449,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.1.0): + jest-config@29.7.0(@types/node@22.4.1): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -14441,7 +14474,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -14470,7 +14503,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -14480,7 +14513,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.1.0 + '@types/node': 22.4.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14519,7 +14552,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -14554,7 +14587,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -14582,7 +14615,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -14628,7 +14661,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -14647,7 +14680,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.1.0 + '@types/node': 22.4.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -14656,23 +14689,23 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.1.0 + '@types/node': 22.4.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.1.0): + jest@29.7.0(@types/node@22.4.1): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.1.0) + jest-cli: 29.7.0(@types/node@22.4.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -15774,6 +15807,14 @@ snapshots: - encoding - supports-color + playwright-core@1.46.1: {} + + playwright@1.46.1: + dependencies: + playwright-core: 1.46.1 + optionalDependencies: + fsevents: 2.3.2 + plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.10 @@ -17189,12 +17230,12 @@ snapshots: dependencies: typescript: 5.5.4 - ts-jest@29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.1.0))(typescript@5.5.4): + ts-jest@29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.4.1))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.1.0) + jest: 29.7.0(@types/node@22.4.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -17352,7 +17393,7 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.13.0: {} + undici-types@6.19.8: {} uni-global@1.0.0: dependencies: diff --git a/react/src/components/UserDropdownMenu.tsx b/react/src/components/UserDropdownMenu.tsx index 088d2b58fa..9117a7cc6a 100644 --- a/react/src/components/UserDropdownMenu.tsx +++ b/react/src/components/UserDropdownMenu.tsx @@ -183,6 +183,7 @@ const UserDropdownMenu: React.FC = () => {