diff --git a/.gitignore b/.gitignore index a547bf3..b88c813 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/assets/chatgpt.jpg b/e2e/assets/chatgpt.jpg new file mode 100644 index 0000000..2aef269 Binary files /dev/null and b/e2e/assets/chatgpt.jpg differ diff --git a/e2e/assets/mountain.jpeg b/e2e/assets/mountain.jpeg new file mode 100644 index 0000000..099114c Binary files /dev/null and b/e2e/assets/mountain.jpeg differ diff --git a/e2e/availabilities.spec.ts b/e2e/availabilities.spec.ts new file mode 100644 index 0000000..f0522f4 --- /dev/null +++ b/e2e/availabilities.spec.ts @@ -0,0 +1,57 @@ +import test, { expect } from "@playwright/test"; +import { APP_URL } from './constants'; + +test('creates an availability', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByRole('link', { name: 'Sales' }).click(); + await page.getByRole('button').first().click(); + await page.getByLabel('Total size').click(); + await page.getByLabel('Total size').fill('0.50'); + await page.getByLabel('Duration').click(); + await page.getByLabel('Duration').fill('30'); + await page.getByLabel('Min price').click(); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Max collateral').click(); + await page.getByLabel('Max collateral').fill('30'); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Nickname').click(); + await page.getByLabel('Nickname').fill('test'); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Confirm your new sale')).toBeVisible(); + await page.locator('small').filter({ hasText: /^512\.0 MB$/ }).click(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.getByText('512.0 MB allocated for the').first()).toBeVisible(); +}) + +test('availability navigation buttons', async ({ page }) => { + await page.goto(APP_URL + '/dashboard/availabilities'); + await page.getByRole('button').first().click(); + await expect(page.locator('.stepper-number-done')).not.toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await page.getByLabel('Total size').click(); + await page.getByLabel('Total size').fill('19'); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled"); + await page.getByLabel('Total size').click(); + await page.getByLabel('Total size').fill('0.5'); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-number-done')).toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await page.getByRole('button', { name: 'Back' }).click(); + await expect(page.locator('.stepper-number-done')).not.toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.locator('.stepper-number-done')).toHaveCount(2) + await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.locator('.modal--open')).not.toBeVisible(); +}) diff --git a/e2e/constants.ts b/e2e/constants.ts new file mode 100644 index 0000000..44414c2 --- /dev/null +++ b/e2e/constants.ts @@ -0,0 +1 @@ +export const APP_URL = "http://localhost:5173" \ No newline at end of file diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts new file mode 100644 index 0000000..e64e07a --- /dev/null +++ b/e2e/settings.spec.ts @@ -0,0 +1,10 @@ +import test, { expect } from "@playwright/test"; +import { APP_URL } from './constants'; + +test('update the log level', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByRole('link', { name: 'Settings' }).click(); + await page.getByLabel('Log level').selectOption('TRACE'); + await page.getByRole('main').locator('div').filter({ hasText: 'Log' }).getByRole('button').click(); + await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible(); +}) \ No newline at end of file diff --git a/e2e/storage-requests.spec.ts b/e2e/storage-requests.spec.ts new file mode 100644 index 0000000..504a547 --- /dev/null +++ b/e2e/storage-requests.spec.ts @@ -0,0 +1,66 @@ +import test, { expect } from "@playwright/test"; +import path, { dirname } from "path"; +import { fileURLToPath } from "url"; +import { APP_URL } from './constants'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +test('creates a storage request', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByRole('link', { name: 'Purchases' }).click(); + await page.getByRole('button', { name: 'Storage Request' }).click(); + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + ]); + await expect(page.locator('#cid')).toHaveValue("zDvZRwzkvwapyNeL4mzw5gBsZvyn7x8F8Y9n4RYSC7ETBssDYpGe") + await expect(page.getByText('Success, the CID has been')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Your request is being processed.')).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.getByText('No data.')).not.toBeVisible(); + await page.getByRole('cell', { name: 'pending' }).getByRole('paragraph').click(); +}) + +test('select a uploaded cid when creating a storage request', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + ]); + await page.getByRole('link', { name: 'Purchases' }).click(); + await page.getByRole('button', { name: 'Storage Request' }).click(); + await page.getByPlaceholder('Select or type your CID').click(); + await page.getByText('N/A0').click(); + await expect(page.getByText('button[disabled]')).not.toBeVisible(); +}) + +test('storage request navigation buttons', async ({ page }) => { + await page.goto(APP_URL + '/dashboard/purchases'); + await page.getByRole('button', { name: 'Storage Request' }).click(); + await expect(page.locator('.stepper-number-done')).not.toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + ]); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.stepper-number-done')).toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await page.getByRole('button', { name: 'Back' }).click(); + await expect(page.locator('.stepper-number-done')).not.toBeVisible() + await expect(page.locator('.stepper-number-active')).toBeVisible() + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.locator('.stepper-number-done')).toHaveCount(2) + await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled"); + await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.locator('.modal--open')).not.toBeVisible(); +}) diff --git a/e2e/upload.spec.ts b/e2e/upload.spec.ts new file mode 100644 index 0000000..785523c --- /dev/null +++ b/e2e/upload.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; +import { Buffer } from 'buffer'; +import { readFileSync } from 'fs'; +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { APP_URL } from './constants'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +test('upload one file', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByText('browse').click(); + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + ]); + + await expect(page.getByText('File uploaded successfully')).toBeVisible(); +}); + +test('multiple files upload', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByText('browse').click(); + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + path.join(__dirname, "assets", 'mountain.jpeg'), + ]); + + await expect(page.getByText('File uploaded successfully').first()).toBeVisible(); + await expect(page.getByText('File uploaded successfully').nth(1)).toBeVisible(); + + + await page.locator('.uploadFile-infoRight > .buttonIcon').first().click(); + await page.locator('.uploadFile-infoRight > .buttonIcon').click(); + + await expect(page.getByText('File uploaded successfully').first()).not.toBeVisible(); + await expect(page.getByText('File uploaded successfully').nth(1)).not.toBeVisible(); +}); + +test('drag and drop file', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + + const buffer = readFileSync(path.join(__dirname, "assets", 'chatgpt.jpg')); + + // Create the DataTransfer and File + const dataTransfer = await page.evaluateHandle((data) => { + const dt = new DataTransfer(); + // Convert the buffer to a hex array + const file = new File([data.toString('hex')], 'chat.jpg', { type: 'image/jpg' }); + dt.items.add(file); + return dt; + }, buffer); + + await page.dispatchEvent('input[type="file"]', 'drop', { dataTransfer }); + + await expect(page.getByText('File uploaded successfully').first()).toBeVisible(); +}); + +test('stop an upload display a message', async ({ page }) => { + await page.goto(APP_URL + '/dashboard'); + await page.getByText('browse').click(); + + const buffer = Buffer.alloc(10_000_000); + + await page.locator('div').getByTestId("upload").setInputFiles({ + buffer, + name: "test.txt", + mimeType: 'text/plain' + }); + + await page.locator('.uploadFile-infoRight > .buttonIcon--small').click(); + + await expect(page.getByText('The upload has been cancelled')).toBeVisible(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 64da9a4..bb5f10a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.3", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.17", + "@codex-storage/marketplace-ui-components": "^0.0.18", "@codex-storage/sdk-js": "^0.0.7", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -23,8 +23,10 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@playwright/test": "^1.48.0", "@tanstack/router-devtools": "^1.58.7", "@tanstack/router-plugin": "^1.58.4", + "@types/node": "^22.7.5", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.7.0", @@ -351,9 +353,9 @@ "dev": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.17.tgz", - "integrity": "sha512-76NHMwL0Ggf8e3I11opvrnJqJ5kKc0sb1uEdsPLTYv+4xS2TCkhQwk+4E8Okw5bUeR1HQwVqj01hpQ1lR1fAVQ==", + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.18.tgz", + "integrity": "sha512-sK7YdlURXqE/GmoFDWJA4X18EdafFgCsJkEsdMyktn05qxCFtN7EGMWZHJrJ1FjlBFNZP5NIMljFrGKfY+xmVg==", "dependencies": { "lucide-react": "^0.441.0" }, @@ -601,6 +603,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", + "dev": true, + "dependencies": { + "playwright": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -1630,6 +1647,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "dev": true, @@ -2609,6 +2635,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -3106,6 +3146,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", + "dev": true, + "dependencies": { + "playwright-core": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.4.45", "dev": true, @@ -3892,6 +3962,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/unplugin": { "version": "1.14.1", "dev": true, diff --git a/package.json b/package.json index 4779705..9fd3459 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "build": "tsc -b && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "format": "prettier --write ./src" + "format": "prettier --write ./src", + "test": "npx playwright test" }, "keywords": [ "Codex", @@ -23,7 +24,7 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.17", + "@codex-storage/marketplace-ui-components": "^0.0.18", "@codex-storage/sdk-js": "^0.0.7", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -37,8 +38,10 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@playwright/test": "^1.48.0", "@tanstack/router-devtools": "^1.58.7", "@tanstack/router-plugin": "^1.58.4", + "@types/node": "^22.7.5", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.7.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e5c3f04 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: '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, + // }, +});