Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions webui/configs/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { SmokeTestOptions } from "./smoke-test.options";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig<SmokeTestOptions>({
testDir: "../test/e2e",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
Expand All @@ -17,18 +16,31 @@ export default defineConfig<SmokeTestOptions>({
/* 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: "https://open-vsx.org",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},

use: { ...devices["Desktop Chrome"] },
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
name: "smoke-test",
testDir: "../test/e2e",
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "https://open-vsx.org",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
}
},
{
name: "api-test",
testDir: "../test/api",
use: {
baseURL: "http://localhost:3000",
}
}
],

// can't define webServer for a project: https://github.com/microsoft/playwright/issues/22496
webServer: {
command: 'yarn prepare && yarn build:default && yarn start:default',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 60 * 1000
}
});
5 changes: 4 additions & 1 deletion webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@types/react-infinite-scroller": "^1.2.3",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.6",
"@types/regex-escape": "^3",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"chai": "^4.3.7",
Expand All @@ -93,6 +94,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.4.0",
"mocha": "^11.7.5",
"regex-escape": "^3.4.11",
"rimraf": "^6.1.2",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.3",
Expand All @@ -106,7 +108,8 @@
"clean": "rimraf lib",
"build": "tsc -p ./tsconfig.json && tsc -p ./configs/server.tsconfig.json && yarn run lint",
"test": "ts-mocha --project ./configs/test.tsconfig.json --config ./configs/mocharc.json",
"smoke-tests": "playwright install && playwright test --config=./configs/playwright.config.ts",
"api-tests": "playwright install && playwright test --config=./configs/playwright.config.ts --project=api-test",
"smoke-tests": "playwright install && playwright test --config=./configs/playwright.config.ts --project=smoke-test",
"lint": "eslint -c ./configs/eslintrc.mjs src",
"watch": "tsc -w -p ./tsconfig.json --preserveWatchOutput",
"prepare": "yarn run clean && yarn run build",
Expand Down
167 changes: 167 additions & 0 deletions webui/test/api/admin-dashboard/extension-admin.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/** ******************************************************************************
* Copyright (c) 2025 Precies. Software OU and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
* ****************************************************************************** */
import { expect, test } from "@playwright/test";
import path from "path"
import user from "../fixtures/admin.json"
import extension from "../fixtures/extension-admin.json"
import RegexEscape from "regex-escape";

test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.route('http://localhost:8080/user', async (route) => route.fulfill({ json: user }))
await page.route('http://localhost:8080/login-providers', async (route) => route.fulfill({ json: { loginProviders: { github: 'https://github.com' } } }))
await page.getByRole('button', { name: 'User Info' }).click();
await page.getByRole('link', { name: 'Admin Dashboard' }).click();
await page.getByRole('button', { name: 'Extensions', exact: true }).click()
})

test('extension admin', async ({ page }) => {
await test.step('get extension', async () => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: extension }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByRole('heading', { name: extension.name, exact: true })).toBeVisible()
await expect(page.getByText(extension.description)).toBeVisible()
for (const targetPlatformVersion of extension.allTargetPlatformVersions) {
const version = targetPlatformVersion.version
await expect(page.getByRole('checkbox', { name: version, exact: true })).toBeVisible()
for (const targetPlatform of targetPlatformVersion.targetPlatforms) {
await expect(page.locator(`input[name="${targetPlatform}/${version}"]`)).toBeVisible()
}
}
})
await test.step('remove extension', async () => {
const tokens = ['delete-extension-csrf', 'forbidden-csrf', 'delete-extension-csrf']
await page.route('http://localhost:8080/user/csrf', async (route) => route.fulfill({ json: { header: 'x-csrf-token', value: tokens.pop() } }))
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer/delete', async (route) => {
const csrfToken = await route.request().headerValue('x-csrf-token')
if (csrfToken !== 'delete-extension-csrf') {
route.fulfill({ status: 403 })
return
}

const json = route.request().postDataJSON()[0].version === '0.4.2712' ? { success: 'Extension version removed' } : { error: 'Extension not found' }
route.fulfill({ json })
})

await page.getByRole('checkbox', { name: '0.4.2601', exact: true }).click()
await expect(page.getByRole('checkbox', { name: '0.4.2601', exact: true })).toBeChecked()
await page.getByText('Remove Versions').click()
await expect(page.getByRole('presentation')).toBeVisible()
await page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true }).click()
await expect(page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true })).toBeDisabled()
await expect(page.getByRole('presentation')).not.toBeVisible()

const version = '0.4.2712'
const allTargetPlatformVersions = extension.allTargetPlatformVersions.filter((value) => value.version !== version)
const removedExtension = extension.allTargetPlatformVersions.find((value) => value.version === version)
const newExtension = { ...extension, allTargetPlatformVersions }
await page.getByRole('checkbox', { name: version, exact: true }).click()
await expect(page.getByRole('checkbox', { name: version, exact: true })).toBeChecked()
await page.getByText('Remove Versions').click()
await expect(page.getByRole('presentation')).toBeVisible()
await expect(page.getByRole('presentation')).toHaveText('Remove 3 versions of rust-analyzer?0.4.2712 (macOS Apple Silicon)0.4.2712 (Linux x64)0.4.2712 (Windows ARM)CancelRemove')
await page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true }).click()
await expect(page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true })).toBeDisabled()

await expect(page.getByText('Request failed: POST http://localhost:8080/admin/extension/rust-lang/rust-analyzer/delete (Forbidden)')).toBeVisible()
await page.getByRole('presentation').getByRole('button', { name: 'Close', exact: true }).click()

await page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true }).click()
await expect(page.getByRole('presentation').getByRole('button', { name: 'Remove', exact: true })).toBeDisabled()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: newExtension }))
await expect(page.getByRole('presentation')).not.toBeVisible()
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByRole('heading', { name: extension.name, exact: true })).toBeVisible()
await expect(page.getByText(extension.description)).toBeVisible()
for (const targetPlatformVersion of newExtension.allTargetPlatformVersions) {
const version = targetPlatformVersion.version
await expect(page.getByRole('checkbox', { name: version, exact: true })).toBeVisible()
for (const targetPlatform of targetPlatformVersion.targetPlatforms) {
await expect(page.locator(`input[name="${targetPlatform}/${version}"]`)).toBeVisible()
}
}

if (removedExtension == null) {
test.fail()
return
}
await expect(page.getByRole('checkbox', { name: removedExtension.version, exact: true })).not.toBeVisible()
for (const targetPlatform of removedExtension.targetPlatforms) {
await expect(page.locator(`input[name="${targetPlatform}/${removedExtension.version}"]`)).not.toBeVisible()
}
})
})

test('no extension found', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ status: 404 }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByText('Extension not found: rust-lang.rust-analyzer')).toBeVisible()
})

test('not admin', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ status: 403 }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByRole('presentation').getByText('Request failed: GET http://localhost:8080/admin/extension/rust-lang/rust-analyzer (Forbidden)')).toBeVisible()
})

test('icon', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: extension }))
await page.route('http://localhost:8080/api/rust-lang/rust-analyzer/darwin-arm64/0.4.2712/file/icon.png', async (route) => route.fulfill({ path: path.normalize(__dirname + '/../fixtures/icon128.png') }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByAltText(extension.name)).toHaveAttribute('src', new RegExp('^' + RegexEscape('blob:http://localhost:3000/')))
})

test('icon not available', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: extension }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByAltText(extension.name)).not.toBeVisible()
})

test('icon not found', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: extension }))
await page.route('http://localhost:8080/api/rust-lang/rust-analyzer/darwin-arm64/0.4.2712/file/icon.png', async (route) => route.fulfill({ status: 404 }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByAltText(extension.name)).not.toBeVisible()
})

test('icon exception', async ({ page }) => {
await page.getByPlaceholder('Namespace').fill('rust-lang')
await page.getByPlaceholder('Extension').fill('rust-analyzer')
await page.getByRole('button', { name: 'Search Extension', exact: true }).click()
await expect(page.getByRole('progressbar')).toBeVisible()
await page.route('http://localhost:8080/admin/extension/rust-lang/rust-analyzer', async (route) => route.fulfill({ json: extension }))
await page.route('http://localhost:8080/api/rust-lang/rust-analyzer/darwin-arm64/0.4.2712/file/icon.png', async (route) => route.fulfill({ status: 400 }))
await expect(page.getByRole('progressbar')).not.toBeVisible()
await expect(page.getByAltText(extension.name)).not.toBeVisible()
})
Loading