-
-
-
-
+
-
+ {/* Section: Flag */}
+
+
+
+
-
-
-
-
+ {/* Section: Files */}
+
+
+
+ {problem.files.length > 0 && (
+
+
+ Existing Files
+
+
+ {problem.files.map(file => (
+
+ ))}
+
+
+ )}
+
- {problem.files.length !== 0 && (
-
-
- Downloads
-
-
- {problem.files.map(file => {
- return (
-
- )
- })}
+ {ProblemActions}
- )}
-
-
-
-
-
- {ProblemActions}
diff --git a/packages/client/testConfig.ts b/packages/client/testConfig.ts
new file mode 100644
index 0000000..041921d
--- /dev/null
+++ b/packages/client/testConfig.ts
@@ -0,0 +1,61 @@
+import dotenv from 'dotenv'
+
+dotenv.config({ path: '.env.test' })
+
+const getVariable = (key: string): string => {
+ const value = process.env[key]
+
+ if (value === undefined) {
+ throw new Error(`Missing environment variable: ${key}`)
+ }
+
+ return value
+}
+
+export interface TestConfig {
+ baseUrl: string
+ ctfName: string
+ homeContent: string
+ loginToken: string
+ divisions: Record
+ testChal: string
+ testChalAns: string
+ testRegEmail: string
+ testRegName: string
+ testUpdateEmail: string
+ testUpdateName: string
+ testNewEmail: string
+ testNewChalName: string
+ testNewChalAuthor: string
+ testNewChalDes: string
+ testNewChalFlag: string
+ regularUserToken: string
+}
+
+const testConfig: TestConfig = {
+ baseUrl: getVariable('BASE_URL'),
+ ctfName: getVariable('NAME'),
+ homeContent: getVariable('HOME_CONTENT'),
+ loginToken: getVariable('LOGIN_TOKEN'),
+ divisions: JSON.parse(getVariable('DIVISIONS')) as Record,
+
+ testChal: getVariable('TEST_CHAL'),
+ testChalAns: getVariable('TEST_CHAL_ANSWER'),
+
+ testRegEmail: getVariable('TEST_REG_EMAIL'),
+ testRegName: getVariable('TEST_REG_NAME'),
+
+ testUpdateEmail: getVariable('TEST_UPDATE_EMAIL'),
+ testUpdateName: getVariable('TEST_UPDATE_NAME'),
+
+ testNewEmail: getVariable('TEST_NEW_MEMBER'),
+
+ testNewChalName: getVariable('TEST_NEW_CHAL_NAME'),
+ testNewChalAuthor: getVariable('TEST_NEW_CHAL_AUTHOR'),
+ testNewChalDes: getVariable('TEST_NEW_CHAL_DES'),
+ testNewChalFlag: getVariable('TEST_NEW_CHAL_FLAG'),
+
+ regularUserToken: getVariable('REGULAR_USER_TOKEN'),
+}
+
+export default testConfig
diff --git a/packages/client/tests/admin.spec.ts b/packages/client/tests/admin.spec.ts
new file mode 100644
index 0000000..23610e6
--- /dev/null
+++ b/packages/client/tests/admin.spec.ts
@@ -0,0 +1,104 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+test.describe('Admin Challenges Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+ await page.fill(
+ 'input[name="teamToken"]',
+ `${testConfig.baseUrl}/login?token=${testConfig.loginToken}`
+ )
+ await page.click('button[type="submit"]')
+ await page.waitForNavigation()
+ await page.goto(`${testConfig.baseUrl}/admin/challs`)
+ })
+
+ test('should display challenge form fields', async ({ page }) => {
+ const problemInput = page
+ .locator('input[placeholder="Problem Name"]')
+ .nth(0)
+ await expect(problemInput).toBeVisible()
+
+ const authorInput = page.locator('input[placeholder="Author"]').nth(0)
+ await expect(authorInput).toBeVisible()
+
+ const descriptionInput = page
+ .locator('textarea[placeholder="Description"]')
+ .nth(0)
+ await expect(descriptionInput).toBeVisible()
+
+ const flagInput = page.locator('input[placeholder="Flag"]').nth(0)
+ await expect(flagInput).toBeVisible()
+
+ const fileInput = page.locator('input[type="file"]').nth(0)
+ await expect(fileInput).toBeVisible()
+
+ const updateButton = page.locator('button:has-text("Update")').nth(0)
+ await expect(updateButton).toBeVisible()
+
+ const deleteButton = page.locator('button:has-text("Delete")').nth(0)
+ await expect(deleteButton).toBeVisible()
+ })
+
+ test('should allow filling challenge details', async ({ page }) => {
+ const problemInput = page
+ .locator('input[placeholder="Problem Name"]')
+ .nth(0)
+ const authorInput = page.locator('input[placeholder="Author"]').nth(0)
+
+ const descriptionInput = page
+ .locator('textarea[placeholder="Description"]')
+ .nth(0)
+
+ const flagInput = page.locator('input[placeholder="Flag"]').nth(0)
+
+ await problemInput.fill(testConfig.testNewChalName)
+ await authorInput.fill(testConfig.testNewChalAuthor)
+ await descriptionInput.fill(testConfig.testNewChalDes)
+ await flagInput.fill(testConfig.testNewChalFlag)
+
+ const tiebreakCheckbox = page.locator('input[type="checkbox"]').nth(0)
+ if (!(await tiebreakCheckbox.isChecked())) {
+ const checkboxId = await tiebreakCheckbox.getAttribute('id')
+ expect(checkboxId).toBeTruthy()
+
+ const label = page.locator(`label[for="${checkboxId as string}"]`)
+ await label.click()
+ }
+
+ await page.click('button:has-text("Update")')
+
+ const successToast = page.locator('.toast--undefined')
+ await expect(successToast).toBeVisible()
+ await expect(successToast).toContainText('Problem successfully updated')
+ })
+
+ test('should allow deleting a challenge', async ({ page }) => {
+ const deleteButton = page.locator('button:has-text("Delete")').nth(0)
+ await expect(deleteButton).toBeVisible()
+
+ await deleteButton.click()
+
+ await page.waitForTimeout(500)
+
+ const confirmDialog = page.locator('button:has-text("Delete Challenge")')
+ if (await confirmDialog.isVisible()) {
+ await confirmDialog.click()
+ }
+
+ const successToast = page.locator('.toast--success')
+ await expect(successToast).toBeVisible()
+ await expect(successToast).toContainText('successfully deleted')
+ })
+})
+
+test.describe('Admin Challenges Page Access Control Tests', () => {
+ test('should redirect unauthorized user to home page', async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+ await page.fill('input[name="teamToken"]', testConfig.regularUserToken)
+ await page.click('button[type="submit"]')
+ await page.waitForNavigation()
+ await page.goto(`${testConfig.baseUrl}/admin/challs`)
+ await expect(page).toHaveURL(`${testConfig.baseUrl}/`)
+ })
+})
diff --git a/packages/client/tests/auth.spec.ts b/packages/client/tests/auth.spec.ts
new file mode 100644
index 0000000..e110a2e
--- /dev/null
+++ b/packages/client/tests/auth.spec.ts
@@ -0,0 +1,119 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+test.describe('rCTF Login Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+ })
+ test('should have the correct page title', async ({ page }) => {
+ await expect(page).toHaveTitle(`Login | ${testConfig.ctfName}`)
+ })
+
+ test('should display the Team Token input field', async ({ page }) => {
+ await expect(page.locator('input[name="teamToken"]')).toBeVisible()
+ })
+
+ test('should log in successfully with a valid Team Token', async ({
+ page,
+ }) => {
+ await page.fill('input[name="teamToken"]', testConfig.loginToken)
+ const submitButton = page.locator('button[type="submit"]')
+ await submitButton.click()
+ await page.waitForNavigation()
+ await expect(page).not.toHaveURL(/login/)
+ })
+
+ test('should show error on invalid Team Token', async ({ page }) => {
+ await page.fill('input[name="teamToken"]', 'invalidTeamToken')
+ await page.click('button[type="submit"]')
+ await expect(page.locator('.text-danger')).toHaveText(
+ /The token provided is invalid./
+ )
+ })
+
+ test('should prevent submission if Team Token is empty', async ({ page }) => {
+ const teamTokenInput = page.locator('input[name="teamToken"]')
+ const submitButton = page.locator('button[type="submit"]')
+
+ await submitButton.click()
+
+ await expect(teamTokenInput).toBeFocused()
+ })
+
+ test('should navigate to recover page when clicking "Lost your team token?" link', async ({
+ page,
+ }) => {
+ const recoverLink = page.locator('a[href="/recover"]')
+ await expect(recoverLink).toBeVisible()
+ await recoverLink.click()
+ await expect(page).toHaveURL(`${testConfig.baseUrl}/recover`)
+ })
+
+ test('should not log in with a invalid Team URL', async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login?token=invalid`)
+ const appDiv = page.locator('#app')
+ const verificationMessage = appDiv.locator('.row.u-center h4')
+ await expect(verificationMessage).toContainText(
+ `Log in to ${testConfig.ctfName}`
+ )
+ })
+
+ test('should go to dashboard page without user login', async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/profile`)
+ await expect(page).toHaveURL(`${testConfig.baseUrl}/`)
+ })
+})
+
+test.describe('rCTF Register Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/register`)
+ })
+
+ test('should have the correct page title', async ({ page }) => {
+ await expect(page).toHaveTitle(/Registration/)
+ })
+
+ test('should display the Team Name & Email input field', async ({ page }) => {
+ await expect(page.locator('input[name="name"]')).toBeVisible()
+ await expect(page.locator('input[name="email"]')).toBeVisible()
+ })
+
+ test('should log in successfully with a valid team name & email', async ({
+ page,
+ }) => {
+ await page.fill('input[name="name"]', testConfig.testRegName)
+ await page.fill('input[name="email"]', testConfig.testRegEmail)
+ await page.click('button[type="submit"]')
+ const appDiv = page.locator('#app')
+ const verificationMessage = appDiv.locator('.row.u-center h3')
+ await expect(verificationMessage).toHaveText('Verification email sent!')
+ })
+
+ test('should prevent submission if Team name or email is empty', async ({
+ page,
+ }) => {
+ const submitButton = page.locator('button[type="submit"]')
+
+ const teamNameInput = page.locator('input[name="name"]')
+ await submitButton.click()
+ await expect(teamNameInput).toBeFocused()
+
+ await page.fill('input[name="name"]', 'test_name')
+ const teamEmailInput = page.locator('input[name="email"]')
+ await submitButton.click()
+ await expect(teamEmailInput).toBeFocused()
+ })
+
+ test('should handle CTFTIME button click and OAuth page has to be opened', async ({
+ page,
+ context,
+ }) => {
+ const [newPage] = await Promise.all([
+ context.waitForEvent('page'),
+ page.locator('div.col-6 > button.c0126').click(),
+ ])
+ await newPage.waitForLoadState()
+ expect(newPage.url()).toMatch(/^https:\/\/oauth\.ctftime\.org\/authorize/)
+ await newPage.close()
+ })
+})
diff --git a/packages/client/tests/challenges.spec.ts b/packages/client/tests/challenges.spec.ts
new file mode 100644
index 0000000..6b8e9b6
--- /dev/null
+++ b/packages/client/tests/challenges.spec.ts
@@ -0,0 +1,108 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+test.describe('Challenges Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+
+ // Fill login token and submit
+ await page.fill('input[name="teamToken"]', testConfig.loginToken)
+ await page.click('button[type="submit"]')
+
+ await page.waitForNavigation()
+
+ await page.goto(`${testConfig.baseUrl}/challs`)
+ })
+
+ test('should render challenges page correctly', async ({ page }) => {
+ await expect(
+ page.locator('.frame__title:has-text("Filters")')
+ ).toBeVisible()
+ await expect(
+ page.locator('.frame__title:has-text("Categories")')
+ ).toBeVisible()
+ })
+
+ test('should have categories and filters', async ({ page }) => {
+ await expect(page.locator('#show-solved')).toBeVisible()
+ })
+
+ test('should toggle all category filters', async ({ page }) => {
+ const categoryCheckboxes = page.locator(
+ '.form-ext-control.form-ext-checkbox input[type="checkbox"]'
+ )
+ const count = await categoryCheckboxes.count()
+
+ for (let i = 0; i < count; i++) {
+ const checkbox = categoryCheckboxes.nth(i)
+
+ const checkboxId = await checkbox.getAttribute('id')
+ expect(checkboxId).toBeTruthy()
+
+ const label = page.locator(`label[for="${checkboxId as string}"]`)
+
+ const categoryName = await label.innerText()
+ console.log(`Toggling filter: ${categoryName}`)
+
+ await expect(checkbox).not.toBeChecked()
+ await label.click()
+ await expect(checkbox).toBeChecked()
+
+ await label.click()
+ await expect(checkbox).not.toBeChecked()
+ }
+ })
+
+ test('should submit a flag for a challenge', async ({ page }) => {
+ const challengeTitle = page.locator(
+ `.frame__title:has-text("${testConfig.testChal}")`
+ )
+ await expect(challengeTitle).toBeVisible()
+
+ const challengeContainer = challengeTitle.locator(
+ 'xpath=ancestor::div[contains(@class, "frame__body")]'
+ )
+ await expect(challengeContainer).toBeVisible()
+
+ const flagInput = challengeContainer.locator('input[placeholder="Flag"]')
+ await flagInput.waitFor({ state: 'visible' })
+
+ // Locate the submit button
+ const submitButton = challengeContainer.locator('button:has-text("Submit")')
+ await expect(submitButton).toBeVisible()
+
+ await flagInput.fill(testConfig.testChalAns)
+ await submitButton.click()
+
+ const successToast = page.locator('text=Flag successfully submitted!')
+ await expect(successToast).toBeVisible()
+ })
+
+ test('should toggle "Show Solved" checkbox', async ({ page }) => {
+ const showSolvedCheckbox = page.locator('#show-solved')
+
+ const showSolvedLabel = page.locator('label[for="show-solved"]')
+ await expect(showSolvedCheckbox).not.toBeChecked()
+
+ await showSolvedLabel.click() // Click the label instead
+ await expect(showSolvedCheckbox).toBeChecked()
+
+ const challengeTitle = page.locator(
+ `.frame__title:has-text("${testConfig.testChal}")`
+ )
+ await expect(challengeTitle).toBeVisible()
+
+ const challengeContainer = challengeTitle.locator(
+ 'xpath=ancestor::div[contains(@class, "frame__body")]'
+ )
+ await expect(challengeContainer).toBeVisible()
+
+ const flagInput = challengeContainer.locator(
+ 'input[placeholder="Flag (solved)"]'
+ )
+ await flagInput.waitFor({ state: 'visible' })
+
+ await showSolvedLabel.click()
+ await expect(showSolvedCheckbox).not.toBeChecked()
+ })
+})
diff --git a/packages/client/tests/home.spec.ts b/packages/client/tests/home.spec.ts
new file mode 100644
index 0000000..588695c
--- /dev/null
+++ b/packages/client/tests/home.spec.ts
@@ -0,0 +1,15 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+test.describe('rCTF Home Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(testConfig.baseUrl)
+ })
+
+ test('should have the configured page description', async ({ page }) => {
+ const appDiv = page.locator('#app')
+ const pageDescription = appDiv.locator('.markup p')
+
+ await expect(pageDescription).toHaveText(testConfig.homeContent)
+ })
+})
diff --git a/packages/client/tests/profile.spec.ts b/packages/client/tests/profile.spec.ts
new file mode 100644
index 0000000..f874bea
--- /dev/null
+++ b/packages/client/tests/profile.spec.ts
@@ -0,0 +1,151 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+interface ClipboardMock {
+ _text: string
+ writeText: (text: string) => Promise
+ readText: () => Promise
+}
+
+test.describe('Profile Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+ await page.fill('input[name="teamToken"]', testConfig.loginToken)
+ await page.click('button[type="submit"]')
+ await page.waitForNavigation()
+ await page.goto(`${testConfig.baseUrl}/profile`)
+ })
+
+ test('Verify profile page is rendered correctly', async ({ page }) => {
+ await expect(page.getByText('Team Invite', { exact: true })).toBeVisible()
+ await expect(page.locator('text=Update Information')).toBeVisible()
+ await expect(
+ page.getByText('Team Information', { exact: true })
+ ).toBeVisible()
+ await expect(page.getByText('Solves', { exact: true })).toBeVisible()
+ await expect(
+ page.getByText('CTFtime Integration', { exact: true })
+ ).toBeVisible()
+ })
+
+ test('should copy invite URL to clipboard when Copy button is clicked', async ({
+ page,
+ }) => {
+ const copyButton = page.locator('button:has-text("Copy")')
+ await expect(copyButton).toBeVisible()
+
+ await page.evaluate(() => {
+ const clipboard: ClipboardMock = {
+ _text: '',
+ async writeText(text: string) {
+ this._text = text
+ },
+ async readText() {
+ return this._text
+ },
+ }
+
+ Object.defineProperty(navigator, 'clipboard', {
+ value: clipboard,
+ configurable: true,
+ })
+ })
+
+ await copyButton.click()
+
+ const copiedText = await page.evaluate(async () =>
+ navigator.clipboard.readText()
+ )
+ expect(copiedText).toContain('/login?token=')
+ })
+
+ test('should reveal and hide the team invite URL when Reveal/Hide button is clicked', async ({
+ page,
+ }) => {
+ const revealButton = page.locator('button:has-text("Reveal")')
+ await expect(revealButton).toBeVisible()
+ await revealButton.click()
+
+ const inviteBlockquote = page.locator('blockquote')
+ await expect(inviteBlockquote).toBeVisible()
+ await expect(inviteBlockquote).toContainText('/login?token=')
+
+ const hideButton = page.locator('button:has-text("Hide")')
+ await expect(hideButton).toBeVisible()
+ await hideButton.click()
+
+ await expect(page.locator('blockquote')).not.toBeVisible()
+ })
+
+ test('should allow updating team information', async ({ page }) => {
+ const teamNameInput = page.locator('input[name="name"]')
+ const emailInput = page.locator('input[name="email"]').nth(0)
+ const divisionSelect = page.locator('select[name="division"]')
+ const updateButton = page.locator('button:has-text("Update")')
+
+ await expect(teamNameInput).toBeVisible()
+ await expect(emailInput).toBeVisible()
+ await expect(divisionSelect).toBeVisible()
+ await expect(updateButton).toBeVisible()
+
+ // Check empty name
+ await teamNameInput.fill('')
+ await updateButton.click()
+ await expect(teamNameInput).toBeFocused()
+
+ await teamNameInput.fill(testConfig.testUpdateName)
+
+ // Check empty email
+ await emailInput.fill('')
+ await updateButton.click()
+ await page.waitForTimeout(1000)
+ const errorToast = page.locator('.toast--error')
+ await expect(errorToast).toBeVisible()
+
+ // Successfull profile update
+ await emailInput.fill(testConfig.testUpdateEmail)
+ await updateButton.click()
+ await page.waitForTimeout(1000)
+ const successToast = page.locator('.toast--undefined')
+ await expect(successToast).toBeVisible()
+
+ await expect(successToast).toContainText('Profile updated')
+ })
+
+ test('should allow add & delete new team member', async ({ page }) => {
+ const emailInput = page.locator('input[name="email"]').nth(1)
+ const updateButton = page.locator('button:has-text("Add member")')
+
+ await expect(emailInput).toBeVisible()
+ await expect(updateButton).toBeVisible()
+
+ // Check empty name
+ await emailInput.fill('')
+ await updateButton.click()
+ await expect(emailInput).toBeFocused()
+
+ await emailInput.fill(testConfig.testNewEmail)
+
+ // Successfull profile update
+ await emailInput.fill(testConfig.testNewEmail)
+ await updateButton.click()
+ await page.waitForTimeout(1000)
+ const addSuccessToast = page.locator('.toast--undefined')
+ await expect(addSuccessToast).toBeVisible()
+ await expect(addSuccessToast).toContainText(
+ 'Team member successfully added'
+ )
+
+ await page.waitForTimeout(4000)
+
+ const deleteButton = page.locator('input[value="Delete"]')
+ await expect(deleteButton).toBeVisible()
+ await deleteButton.click()
+ await page.waitForTimeout(1000)
+ const deleteSuccessToast = page.locator('.toast--undefined')
+ await expect(deleteSuccessToast).toBeVisible()
+ await expect(addSuccessToast).toContainText(
+ 'Team member successfully deleted'
+ )
+ })
+})
diff --git a/packages/client/tests/scoreboard.spec.ts b/packages/client/tests/scoreboard.spec.ts
new file mode 100644
index 0000000..ba646fe
--- /dev/null
+++ b/packages/client/tests/scoreboard.spec.ts
@@ -0,0 +1,84 @@
+import { test, expect } from '@playwright/test'
+import testConfig from '../testConfig'
+
+test.describe('Scoreboard Page Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${testConfig.baseUrl}/login`)
+ await page.fill('input[name="teamToken"]', testConfig.loginToken)
+ await page.click('button[type="submit"]')
+ await page.waitForNavigation()
+ await page.goto(`${testConfig.baseUrl}/scores`)
+ })
+
+ test('should render scoreboard with teams', async ({ page }) => {
+ await expect(page.locator('table')).toBeVisible()
+ await expect(page.locator('thead tr')).toHaveCount(1)
+ const rowCount = await page.locator('table tr').count()
+ expect(rowCount).toBeGreaterThan(1)
+ })
+
+ test('should render scoreboard with teams after filter applied', async ({
+ page,
+ }) => {
+ await expect(page.locator('table')).toBeVisible()
+ await expect(page.locator('thead tr')).toHaveCount(1)
+
+ const initialRowCount = await page.locator('table tr').count()
+ expect(initialRowCount).toBeGreaterThan(1)
+
+ const divisionOptions: string[] = Object.values(testConfig.divisions)
+ if (divisionOptions.length > 0) {
+ await page.selectOption('select[name="division"]', divisionOptions[0])
+ } else {
+ console.log('No divisions available in testConfig')
+ }
+
+ await page.waitForTimeout(500)
+
+ const filteredRowCount = await page.locator('table tr').count()
+ await expect(page.locator('table')).toBeVisible()
+ expect(filteredRowCount).toBeGreaterThan(0)
+ })
+
+ test('should render scoreboard with teams after limit is changed', async ({
+ page,
+ }) => {
+ await expect(page.locator('table')).toBeVisible()
+ await expect(page.locator('thead tr')).toHaveCount(1)
+
+ const initialRowCount = await page.locator('table tr').count()
+ expect(initialRowCount).toBeGreaterThan(1)
+
+ await page.selectOption('select[name="pagesize"]', '50')
+
+ await page.waitForTimeout(500)
+
+ const filteredRowCount = await page.locator('table tr').count()
+
+ await expect(page.locator('table')).toBeVisible()
+ expect(filteredRowCount).toBeGreaterThan(0)
+ })
+
+ test('should scroll to user’s team when "Go to my team" button is clicked', async ({
+ page,
+ }) => {
+ const button = page.locator('button', { hasText: 'GO TO MY TEAM' })
+
+ await expect(button).toBeVisible()
+
+ const isEnabled = await button.isEnabled()
+ if (isEnabled) {
+ const initialScrollY = await page.evaluate(() => window.scrollY)
+
+ await button.click()
+
+ await page.waitForTimeout(500)
+
+ const newScrollY = await page.evaluate(() => window.scrollY)
+
+ expect(newScrollY).toBeGreaterThanOrEqual(initialScrollY)
+ } else {
+ console.log('Button is disabled, skipping click.')
+ }
+ })
+})
diff --git a/packages/client/tsconfig.playwright.json b/packages/client/tsconfig.playwright.json
new file mode 100644
index 0000000..229e441
--- /dev/null
+++ b/packages/client/tsconfig.playwright.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig-base.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2020",
+ "lib": ["es2020", "dom"],
+ "types": ["node", "@playwright/test"],
+ "noEmit": true
+ },
+ "include": ["tests/**/*.ts", "testConfig.ts", "playwright.config.ts"]
+}
diff --git a/yarn.lock b/yarn.lock
index f108c12..3ef2d74 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3428,6 +3428,17 @@ __metadata:
languageName: node
linkType: hard
+"@playwright/test@npm:1.50.1":
+ version: 1.50.1
+ resolution: "@playwright/test@npm:1.50.1"
+ dependencies:
+ playwright: "npm:1.50.1"
+ bin:
+ playwright: cli.js
+ checksum: 10/0d8d2291d6554c492cb163b4d463e1e9cc6d3ae50680d790473f693f36a243c16c3620406849dd40115046c47a6ad5cc36a24511caec6d054dc1a1d9fffb4138
+ languageName: node
+ linkType: hard
+
"@pmmmwh/react-refresh-webpack-plugin@npm:^0.4.3":
version: 0.4.3
resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.4.3"
@@ -3532,6 +3543,7 @@ __metadata:
"@babel/preset-react": "npm:7.16.0"
"@babel/preset-typescript": "npm:7.16.0"
"@emotion/jest": "npm:11.5.0"
+ "@playwright/test": "npm:1.50.1"
"@prefresh/babel-plugin": "npm:0.4.1"
"@prefresh/webpack": "npm:3.3.2"
"@reach/auto-id": "npm:0.16.0"
@@ -3547,6 +3559,7 @@ __metadata:
"@testing-library/preact": "npm:2.0.1"
"@testing-library/user-event": "npm:13.5.0"
"@theme-ui/preset-base": "npm:0.9.1"
+ "@types/node": "npm:16.11.6"
"@types/react": "npm:17.0.33"
"@types/webpack-env": "npm:1.16.3"
babel-loader: "npm:8.2.3"
@@ -11760,6 +11773,16 @@ __metadata:
languageName: node
linkType: hard
+"fsevents@npm:2.3.2, fsevents@npm:^2.1.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
+ version: 2.3.2
+ resolution: "fsevents@npm:2.3.2"
+ dependencies:
+ node-gyp: "npm:latest"
+ checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
+ conditions: os=darwin
+ languageName: node
+ linkType: hard
+
"fsevents@npm:^1.2.7":
version: 1.2.13
resolution: "fsevents@npm:1.2.13"
@@ -11771,12 +11794,11 @@ __metadata:
languageName: node
linkType: hard
-"fsevents@npm:^2.1.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
+"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.1.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin":
version: 2.3.2
- resolution: "fsevents@npm:2.3.2"
+ resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
- checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
conditions: os=darwin
languageName: node
linkType: hard
@@ -11791,15 +11813,6 @@ __metadata:
languageName: node
linkType: hard
-"fsevents@patch:fsevents@npm%3A^2.1.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin":
- version: 2.3.2
- resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1"
- dependencies:
- node-gyp: "npm:latest"
- conditions: os=darwin
- languageName: node
- linkType: hard
-
"function-bind@npm:^1.1.1":
version: 1.1.1
resolution: "function-bind@npm:1.1.1"
@@ -18037,6 +18050,30 @@ __metadata:
languageName: node
linkType: hard
+"playwright-core@npm:1.50.1":
+ version: 1.50.1
+ resolution: "playwright-core@npm:1.50.1"
+ bin:
+ playwright-core: cli.js
+ checksum: 10/9a310b8a66bf7fd926e620c1c8e27be29bdbdce91640e5f975b2fd4dc706d0307faec2bb0456cc8e7dedb1e71c0b5eb35c6a58acd5cedc7d8fd849a9067e637b
+ languageName: node
+ linkType: hard
+
+"playwright@npm:1.50.1":
+ version: 1.50.1
+ resolution: "playwright@npm:1.50.1"
+ dependencies:
+ fsevents: "npm:2.3.2"
+ playwright-core: "npm:1.50.1"
+ dependenciesMeta:
+ fsevents:
+ optional: true
+ bin:
+ playwright: cli.js
+ checksum: 10/a3687614ac3238a81cbe3018e4f4a2ae92c71f3f65110cc6087068c020f6134f0628308da33177b9b08102644706e835d4053f6890beeb4a935f433bc4ac107a
+ languageName: node
+ linkType: hard
+
"please-upgrade-node@npm:^3.2.0":
version: 3.2.0
resolution: "please-upgrade-node@npm:3.2.0"