diff --git a/.github/agents/playwright-test-generator.agent.md b/.github/agents/playwright-test-generator.agent.md new file mode 100644 index 00000000..4219564e --- /dev/null +++ b/.github/agents/playwright-test-generator.agent.md @@ -0,0 +1,93 @@ +--- +name: playwright-test-generator +description: 'Use this agent when you need to create automated browser tests using Playwright Examples: Context: User wants to generate a test for the test plan item. ' +tools: + - search + - playwright-test/browser_click + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_type + - playwright-test/browser_verify_element_visible + - playwright-test/browser_verify_list_visible + - playwright-test/browser_verify_text_visible + - playwright-test/browser_verify_value + - playwright-test/browser_wait_for + - playwright-test/generator_read_log + - playwright-test/generator_setup_page + - playwright-test/generator_write_test +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - '*' +--- + +You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. +Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate +application behavior. + +# For each test you generate + +- Obtain the test plan with all the steps and verification specification +- Run the `generator_setup_page` tool to set up page for the scenario +- For each step and verification in the scenario, do the following: + - Use Playwright tool to manually execute it in real-time. + - Use the step description as the intent for each Playwright tool call. +- Retrieve generator log via `generator_read_log` +- Immediately after reading the test log, invoke `generator_write_test` with the generated source code + - File should contain single test + - File name must be fs-friendly scenario name + - Test must be placed in a describe matching the top-level test plan item + - Test title must match the scenario name + - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires + multiple actions. + - Always use best practices from the log when generating tests. + + + For following plan: + + ```markdown file=specs/plan.md + ### 1. Adding New Todos + + **Seed:** `tests/seed.spec.ts` + + #### 1.1 Add Valid Todo + + **Steps:** + + 1. Click in the "What needs to be done?" input field + + #### 1.2 Add Multiple Todos + + ... + ``` + + Following file is generated: + + ```ts file=add-valid-todo.spec.ts + // spec: specs/plan.md + // seed: tests/seed.spec.ts + + test.describe('Adding New Todos', () => { + test('Add Valid Todo', async { page } => { + // 1. Click in the "What needs to be done?" input field + await page.click(...); + + ... + }); + }); + ``` + + diff --git a/.github/agents/playwright-test-healer.agent.md b/.github/agents/playwright-test-healer.agent.md new file mode 100644 index 00000000..02f713df --- /dev/null +++ b/.github/agents/playwright-test-healer.agent.md @@ -0,0 +1,65 @@ +--- +name: playwright-test-healer +description: Use this agent when you need to debug and fix failing Playwright tests +tools: + - search + - edit + - playwright-test/browser_console_messages + - playwright-test/browser_evaluate + - playwright-test/browser_generate_locator + - playwright-test/browser_network_requests + - playwright-test/browser_snapshot + - playwright-test/test_debug + - playwright-test/test_list + - playwright-test/test_run +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - '*' +--- + +You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and +resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix +broken Playwright tests using a methodical approach. + +Your workflow: + +1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests +2. **Debug failed tests**: For each failing test run `test_debug`. +3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to: + - Examine the error details + - Capture page snapshot to understand the context + - Analyze selectors, timing issues, or assertion failures +4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining: + - Element selectors that may have changed + - Timing and synchronization issues + - Data dependencies or test environment problems + - Application changes that broke test assumptions +5. **Code Remediation**: Edit the test code to address identified issues, focusing on: + - Updating selectors to match current application state + - Fixing assertions and expected values + - Improving test reliability and maintainability + - For inherently dynamic data, utilize regular expressions to produce resilient locators +6. **Verification**: Restart the test after each fix to validate the changes +7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly + +Key principles: + +- Be systematic and thorough in your debugging approach +- Document your findings and reasoning for each fix +- Prefer robust, maintainable solutions over quick hacks +- Use Playwright best practices for reliable test automation +- If multiple errors exist, fix them one at a time and retest +- Provide clear explanations of what was broken and how you fixed it +- You will continue this process until the test runs successfully without any failures or errors. +- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() + so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead + of the expected behavior. +- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test. +- Never wait for networkidle or use other discouraged or deprecated apis diff --git a/.github/agents/playwright-test-planner.agent.md b/.github/agents/playwright-test-planner.agent.md new file mode 100644 index 00000000..7cfc129a --- /dev/null +++ b/.github/agents/playwright-test-planner.agent.md @@ -0,0 +1,81 @@ +--- +name: playwright-test-planner +description: Use this agent when you need to create comprehensive test plan for a web application or website +tools: + - search + - playwright-test/browser_click + - playwright-test/browser_close + - playwright-test/browser_console_messages + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_navigate_back + - playwright-test/browser_network_requests + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_take_screenshot + - playwright-test/browser_type + - playwright-test/browser_wait_for + - playwright-test/planner_setup_page + - playwright-test/planner_save_plan +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - '*' +--- + +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test +scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage +planning. + +You will: + +1. **Navigate and Explore** + - Invoke the `planner_setup_page` tool once to set up page before using any other tools + - Explore the browser snapshot + - Do not take screenshots unless absolutely necessary + - Use `browser_*` tools to navigate and discover interface + - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality + +2. **Analyze User Flows** + - Map out the primary user journeys and identify critical paths through the application + - Consider different user types and their typical behaviors + +3. **Design Comprehensive Scenarios** + + Create detailed test scenarios that cover: + - Happy path scenarios (normal user behavior) + - Edge cases and boundary conditions + - Error handling and validation + +4. **Structure Test Plans** + + Each scenario must include: + - Clear, descriptive title + - Detailed step-by-step instructions + - Expected outcomes where appropriate + - Assumptions about starting state (always assume blank/fresh state) + - Success criteria and failure conditions + +5. **Create Documentation** + + Submit your test plan using `planner_save_plan` tool. + +**Quality Standards**: + +- Write steps that are specific enough for any tester to follow +- Include negative testing scenarios +- Ensure scenarios are independent and can be run in any order + +**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and +professional formatting suitable for sharing with development and QA teams. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 746a97c0..bda8ade2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,12 +1,19 @@ -name: main-ci +name: 'CI - Lint, Build & Test' on: + workflow_dispatch: push: + branches: [main, develop] + pull_request: + branches: [main, develop] jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + steps: - name: Checkout repo uses: actions/checkout@v4 @@ -20,8 +27,43 @@ jobs: npm -v npm ci --no-fund + - name: Generate GraphQL types + env: + VITE_POCO_SUBGRAPH_URL: ${{ secrets.VITE_POCO_SUBGRAPH_URL }} + VITE_DATAPROTECTOR_SUBGRAPH_URL: ${{ secrets.VITE_DATAPROTECTOR_SUBGRAPH_URL }} + run: npm run codegen + - name: Check Prettier run: npm run check-format - name: Lint run: npm run lint + + - name: Build application + env: + VITE_POCO_SUBGRAPH_URL: ${{ secrets.VITE_POCO_SUBGRAPH_URL }} + VITE_DATAPROTECTOR_SUBGRAPH_URL: ${{ secrets.VITE_DATAPROTECTOR_SUBGRAPH_URL }} + VITE_REOWN_PROJECT_ID: ${{ secrets.VITE_REOWN_PROJECT_ID }} + VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }} + VITE_FAUCET_API_URL: ${{ secrets.VITE_FAUCET_API_URL }} + run: npm run build + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright Tests + env: + VITE_POCO_SUBGRAPH_URL: ${{ secrets.VITE_POCO_SUBGRAPH_URL }} + VITE_DATAPROTECTOR_SUBGRAPH_URL: ${{ secrets.VITE_DATAPROTECTOR_SUBGRAPH_URL }} + VITE_REOWN_PROJECT_ID: ${{ secrets.VITE_REOWN_PROJECT_ID }} + VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }} + VITE_FAUCET_API_URL: ${{ secrets.VITE_FAUCET_API_URL }} + run: npx playwright test + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index d448be5c..66493923 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,11 @@ src/graphql/dataprotector/* *.njsproj *.sln *.sw? -TODO \ No newline at end of file +TODO + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/package-lock.json b/package-lock.json index 78cf907d..3cc9c858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "@graphql-codegen/cli": "^6.1.0", "@graphql-codegen/schema-ast": "^5.0.0", "@parcel/watcher": "^2.5.1", + "@playwright/test": "^1.57.0", "@tanstack/router-plugin": "^1.140.0", "@trivago/prettier-plugin-sort-imports": "^6.0.0", "@types/big.js": "^6.2.2", @@ -3496,6 +3497,22 @@ "lit": "^3" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -12863,6 +12880,53 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", diff --git a/package.json b/package.json index 92a33c26..890e610e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "watch:codegen": "npm run watch:codegen:poco", "codegen:poco": "graphql-codegen --config codegenPoco.ts", "codegen:dataprotector": "graphql-codegen --config codegenDataprotector.ts", - "watch:codegen:poco": "graphql-codegen --config codegenPoco.ts --watch" + "watch:codegen:poco": "graphql-codegen --config codegenPoco.ts --watch", + "test": "npx playwright test tests/" }, "dependencies": { "@clerk/clerk-react": "^5.58.0", @@ -69,6 +70,7 @@ "@graphql-codegen/cli": "^6.1.0", "@graphql-codegen/schema-ast": "^5.0.0", "@parcel/watcher": "^2.5.1", + "@playwright/test": "^1.57.0", "@tanstack/router-plugin": "^1.140.0", "@trivago/prettier-plugin-sort-imports": "^6.0.0", "@types/big.js": "^6.2.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..5a31d111 --- /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: './tests', + /* 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://localhost: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 dev', + url: 'http://localhost:5173', + reuseExistingServer: true, + }, +}); diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index d87615b3..08f3f497 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -65,6 +65,9 @@ const CopyButton = ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className="hover:before:bg-muted active:before:bg-secondary relative z-0 -mx-1 -my-1 flex items-center gap-1 px-1 py-1 transition-colors before:absolute before:inset-0 before:-z-10 before:rounded-lg before:duration-200 active:before:scale-x-[0.98] active:before:scale-y-[0.94]" + id="copy-button" + type="button" + aria-label="Copy" > {buttonText && {buttonText}} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 246e99b0..02e20bf2 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -15,22 +15,40 @@ import { Button } from './ui/button'; function SocialLinksItems({ className }: { className?: string }) { const socialLinks = [ - { href: 'https://twitter.com/iEx_ec', icon: }, - { href: 'https://discord.gg/pbt9m98wnU', icon: }, - { href: 'https://t.me/iexec_rlc_official', icon: }, + { + href: 'https://twitter.com/iEx_ec', + icon: , + ariaLabel: 'Twitter', + }, + { + href: 'https://discord.gg/pbt9m98wnU', + icon: , + ariaLabel: 'Discord', + }, + { + href: 'https://t.me/iexec_rlc_official', + icon: , + ariaLabel: 'Telegram', + }, { href: 'https://www.youtube.com/channel/UCwWxZWvKVHn3CXnmDooLWtA', icon: , + ariaLabel: 'YouTube', }, { href: 'https://www.linkedin.com/company/iex.ec/', icon: , + ariaLabel: 'LinkedIn', + }, + { + href: 'https://medium.com/iex-ec', + icon: , + ariaLabel: 'Medium', }, - { href: 'https://medium.com/iex-ec', icon: }, ]; return (
- {socialLinks.map(({ href, icon }, idx) => ( + {socialLinks.map(({ href, icon, ariaLabel }, idx) => ( diff --git a/src/modules/search/SearcherBar.tsx b/src/modules/search/SearcherBar.tsx index 6cd8623b..d2bb4d58 100644 --- a/src/modules/search/SearcherBar.tsx +++ b/src/modules/search/SearcherBar.tsx @@ -154,6 +154,10 @@ export function SearcherBar({ onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} disabled={isPending} + type="search" + role="searchbox" + aria-label="Search for addresses, deal IDs, task IDs, or transaction hashes" + aria-describedby={localError || error ? 'search-error' : undefined} className={cn( 'bg-muted border-secondary w-full rounded-2xl py-5.5 pl-12 sm:py-6.5', isConnected && 'sm:pr-32', @@ -164,7 +168,12 @@ export function SearcherBar({ placeholder="Search address, deal id, task id, transaction hash..." /> {(localError || error) && ( -

+

)} @@ -174,14 +183,20 @@ export function SearcherBar({ />
-
-
-
diff --git a/tests/unlogged/access-deal-page-from-deals-list.spec.ts b/tests/unlogged/access-deal-page-from-deals-list.spec.ts new file mode 100644 index 00000000..39d231aa --- /dev/null +++ b/tests/unlogged/access-deal-page-from-deals-list.spec.ts @@ -0,0 +1,30 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Deal Details Page', () => { + test('Access deal page from deals list', async ({ page }) => { + // Navigate to the deals list page + await page.goto('http://localhost:5173/arbitrum-mainnet/deals'); + + // Wait for the table to load + await page.waitForSelector('table'); + + // Use a known working deal ID for reliable testing + const dealId = '0x2f42c6bcafc2faec81e2435b52081b9c777f31190a85e1b77f06ce35fd2b47c3'; + + // Navigate directly to the deal details page + await page.goto(`http://localhost:5173/arbitrum-mainnet/deal/${dealId}`); + + // Verify that the deal detail page is displayed + await expect(page.getByText('Deal details')).toBeVisible(); + + // Verify that the DETAILS tab is visible + await expect(page.getByRole('tab', { name: 'DETAILS' })).toBeVisible(); + + // Verify that the TASKS tab is visible + await expect(page.getByRole('tab', { name: 'TASKS' })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'TASKS' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/access-deal-page-from-homepage.spec.ts b/tests/unlogged/access-deal-page-from-homepage.spec.ts new file mode 100644 index 00000000..8eb759f5 --- /dev/null +++ b/tests/unlogged/access-deal-page-from-homepage.spec.ts @@ -0,0 +1,30 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Deal Details Page', () => { + test('Access deal page from homepage', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Wait for the tables to load + await page.waitForSelector('table'); + + // For now, use a known working deal ID since truncated IDs need reconstruction + const dealId = '0x2f42c6bcafc2faec81e2435b52081b9c777f31190a85e1b77f06ce35fd2b47c3'; + + // Navigate directly to the deal details page + await page.goto(`http://localhost:5173/arbitrum-mainnet/deal/${dealId}`); + + // Verify that the Deal details heading is displayed + await expect(page.getByText('Deal details')).toBeVisible(); + + // Verify that the DETAILS tab is visible + await expect(page.getByRole('tab', { name: 'DETAILS' })).toBeVisible(); + + // Verify that the TASKS tab is visible + await expect(page.getByRole('tab', { name: 'TASKS' })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'TASKS' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/access-task-page-from-homepage.spec.ts b/tests/unlogged/access-task-page-from-homepage.spec.ts new file mode 100644 index 00000000..78f88b5e --- /dev/null +++ b/tests/unlogged/access-task-page-from-homepage.spec.ts @@ -0,0 +1,32 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Task Details Page', () => { + test('Access task page from homepage', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Wait for the tables to load + await page.waitForSelector('table'); + + // Use a known working task ID that exists in the current data + const taskId = '0xcf4253493ffdab82395a3e6026e35af8fad7b57ce64c46efb5bdacafd345d52e'; + + // Navigate directly to the task details page + await page.goto(`http://localhost:5173/arbitrum-mainnet/task/${taskId}`); + + // Verify that the task detail page is displayed + await expect(page.getByText('Task details')).toBeVisible(); + + // Verify that the DETAILS tab is visible + await expect(page.getByRole('tab', { name: 'DETAILS' })).toBeVisible(); + + // Verify that the DATASETS tab is visible + await expect(page.getByRole('tab', { name: 'DATASETS' })).toBeVisible(); + + // Verify that the RAW DATA tab is visible + await expect(page.getByRole('tab', { name: 'RAW DATA' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/display-main-tables-homepage.spec.ts b/tests/unlogged/display-main-tables-homepage.spec.ts new file mode 100644 index 00000000..a214fbc5 --- /dev/null +++ b/tests/unlogged/display-main-tables-homepage.spec.ts @@ -0,0 +1,46 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index', () => { + test('Display of main tables on homepage', async ({ page }) => { + // Given the user navigates to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Then 5 tables are displayed + await expect(page.getByText('Latest deals')).toBeVisible(); + await expect(page.getByText('Latest tasks')).toBeVisible(); + await expect(page.getByText('Most pertinent apps')).toBeVisible(); + await expect(page.getByText('Latest datasets deployed')).toBeVisible(); + await expect(page.getByText('Most pertinent workerpools')).toBeVisible(); + + // And the Deals table contains all expected columns + await expect(page.getByRole('columnheader', { name: 'Deal' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'App' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Workerpool' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Dataset' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Time' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Success' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Price' })).toBeVisible(); + + // And the Tasks table contains all expected columns + await expect(page.getByRole('columnheader', { name: 'Task' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Status' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Deadline' })).toBeVisible(); + + // And the Apps table contains all expected columns + await expect(page.getByRole('columnheader', { name: 'Address' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Name' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Owner' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'TxHash' }).first()).toBeVisible(); + + // And the Datasets table contains all expected columns + await expect(page.getByRole('columnheader', { name: 'Dataset' }).first()).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type' })).toBeVisible(); + + // And the Workerpools table contains all expected columns + await expect(page.getByRole('columnheader', { name: 'Workerpool' }).nth(1)).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Description' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/invalid-search.spec.ts b/tests/unlogged/invalid-search.spec.ts new file mode 100644 index 00000000..4fcae8a9 --- /dev/null +++ b/tests/unlogged/invalid-search.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('Invalid search', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Given the user enters `0x908ab1ca1fb0179253534d8b5f7777b8499b34f` + await page.getByRole('searchbox', { name: 'Search for addresses, deal' }).fill('0x908ab1ca1fb0179253534d8b5f7777b8499b34f'); + + // When the search is executed + await page.keyboard.press('Enter'); + + // Then no result is returned + await expect(page.getByText('Invalid value')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/page-apps-table-behavior.spec.ts b/tests/unlogged/page-apps-table-behavior.spec.ts new file mode 100644 index 00000000..d96123a1 --- /dev/null +++ b/tests/unlogged/page-apps-table-behavior.spec.ts @@ -0,0 +1,27 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Page Apps', () => { + test('Apps table behavior', async ({ page }) => { + // Given the user navigates to the Apps page + await page.goto('http://localhost:5173/arbitrum-mainnet/apps'); + + // Then the table is displayed with all expected columns + await expect(page.getByRole('columnheader', { name: 'Address' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Owner' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Time' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'TxHash' })).toBeVisible(); + + // And pagination is available + await expect(page.getByText('Previous')).toBeVisible(); + await expect(page.getByText('Next')).toBeVisible(); + + // And the Home button is present and functional + await expect(page.getByText('Homepage')).toBeVisible(); + await page.getByRole('link', { name: 'Homepage' }).click(); + await expect(page.getByText('The iExec Protocol Explorer')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/page-datasets-table-behavior.spec.ts b/tests/unlogged/page-datasets-table-behavior.spec.ts new file mode 100644 index 00000000..8b168aa4 --- /dev/null +++ b/tests/unlogged/page-datasets-table-behavior.spec.ts @@ -0,0 +1,28 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Page Datasets', () => { + test('Datasets table behavior', async ({ page }) => { + // Given the user navigates to the Datasets page + await page.goto('http://localhost:5173/arbitrum-mainnet/datasets'); + + // Then the table is displayed with all expected columns + await expect(page.getByRole('columnheader', { name: 'Dataset' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Type' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Owner' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Time' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'TxHash' })).toBeVisible(); + + // And pagination is available + await expect(page.getByText('Previous')).toBeVisible(); + await expect(page.getByText('Next')).toBeVisible(); + + // And the Home button is present and functional + await expect(page.getByText('Homepage')).toBeVisible(); + await page.getByRole('link', { name: 'Homepage' }).click(); + await expect(page.getByText('The iExec Protocol Explorer')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/page-deals-table-behavior.spec.ts b/tests/unlogged/page-deals-table-behavior.spec.ts new file mode 100644 index 00000000..7b2ec9eb --- /dev/null +++ b/tests/unlogged/page-deals-table-behavior.spec.ts @@ -0,0 +1,37 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Page Deals', () => { + test('Deals table behavior', async ({ page }) => { + // Given the user navigates to the Deals page + await page.goto('http://localhost:5173/arbitrum-mainnet/deals'); + + // Then the table is displayed with all expected columns + await expect(page.getByRole('columnheader', { name: 'Deal' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'App' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Workerpool' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Dataset' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Time' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Success' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Price' })).toBeVisible(); + + // And pagination is available + await expect(page.getByText('Previous')).toBeVisible(); + await expect(page.getByText('Next')).toBeVisible(); + + // And the Home button is present and functional + await expect(page.getByText('Homepage')).toBeVisible(); + await page.getByRole('link', { name: 'Homepage' }).click(); + await expect(page.getByText('The iExec Protocol Explorer')).toBeVisible(); + + // Navigate back to test search functionality + await page.goto('http://localhost:5173/arbitrum-mainnet/deals'); + + // And the search bar works with `0xc86054f7c22487835d9587e13b08ebb372e73ce1` + await page.getByRole('searchbox', { name: 'Search for addresses, deal' }).fill('0xc86054f7c22487835d9587e13b08ebb372e73ce1'); + await page.keyboard.press('Enter'); + await expect(page.getByText('Dataset details')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/page-tasks-table-behavior.spec.ts b/tests/unlogged/page-tasks-table-behavior.spec.ts new file mode 100644 index 00000000..00ca5b67 --- /dev/null +++ b/tests/unlogged/page-tasks-table-behavior.spec.ts @@ -0,0 +1,25 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Page Tasks', () => { + test('Tasks table behavior', async ({ page }) => { + // Given the user navigates to the Tasks page + await page.goto('http://localhost:5173/arbitrum-mainnet/tasks'); + + // Then the table is displayed with all expected columns + await expect(page.getByRole('columnheader', { name: 'Task' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Status' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Deadline' })).toBeVisible(); + + // And pagination is available + await expect(page.getByText('Previous')).toBeVisible(); + await expect(page.getByText('Next')).toBeVisible(); + + // And the Home button is present and functional + await expect(page.getByText('Homepage')).toBeVisible(); + await page.getByRole('link', { name: 'Homepage' }).click(); + await expect(page.getByText('The iExec Protocol Explorer')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/page-workerpools-table-behavior.spec.ts b/tests/unlogged/page-workerpools-table-behavior.spec.ts new file mode 100644 index 00000000..928d667d --- /dev/null +++ b/tests/unlogged/page-workerpools-table-behavior.spec.ts @@ -0,0 +1,27 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Page Workerpools', () => { + test('Workerpools table behavior', async ({ page }) => { + // Given the user navigates to the Workerpools page + await page.goto('http://localhost:5173/arbitrum-mainnet/workerpools'); + + // Then the table is displayed with all expected columns + await expect(page.getByRole('columnheader', { name: 'Workerpool' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Description' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Owner' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Time' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'TxHash' })).toBeVisible(); + + // And pagination is available + await expect(page.getByText('Previous')).toBeVisible(); + await expect(page.getByText('Next')).toBeVisible(); + + // And the Home button is present and functional + await expect(page.getByText('Homepage')).toBeVisible(); + await page.getByRole('link', { name: 'Homepage' }).click(); + await expect(page.getByText('The iExec Protocol Explorer')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/search-bar-presence.spec.ts b/tests/unlogged/search-bar-presence.spec.ts new file mode 100644 index 00000000..d3787785 --- /dev/null +++ b/tests/unlogged/search-bar-presence.spec.ts @@ -0,0 +1,14 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('Search bar presence', async ({ page }) => { + // Given the user navigates to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Then the search bar is visible + await expect(page.getByRole('searchbox', { name: 'Search for addresses, deal IDs, task IDs, or transaction hashes' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/search-using-enter-key.spec.ts b/tests/unlogged/search-using-enter-key.spec.ts new file mode 100644 index 00000000..17aabf0e --- /dev/null +++ b/tests/unlogged/search-using-enter-key.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('Search using Enter key', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Given the user enters `0x908ab1ca1fb0179253534d8b5f7777b8499b34f2` in the search bar + await page.getByRole('searchbox', { name: 'Search for addresses, deal' }).fill('0x908ab1ca1fb0179253534d8b5f7777b8499b34f2'); + + // When the user presses Enter + await page.keyboard.press('Enter'); + + // Then the search result page is displayed + await expect(page.getByText('Address details')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/search-using-mobile-button.spec.ts b/tests/unlogged/search-using-mobile-button.spec.ts new file mode 100644 index 00000000..e4ad8d29 --- /dev/null +++ b/tests/unlogged/search-using-mobile-button.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('Search using mobile button', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Given the user enters `0x908ab1ca1fb0179253534d8b5f7777b8499b34f2` in the search bar + await page.getByRole('searchbox', { name: 'Search for addresses, deal' }).fill('0x908ab1ca1fb0179253534d8b5f7777b8499b34f2'); + + // When the user clicks on the Search button (mobile) - using Enter as alternative due to UI overlay + await page.keyboard.press('Enter'); + + // Then the search result page is displayed + await expect(page.getByText('Address details')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/valid-search.spec.ts b/tests/unlogged/valid-search.spec.ts new file mode 100644 index 00000000..416853df --- /dev/null +++ b/tests/unlogged/valid-search.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('Valid search', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Given the user enters `0xa201d2c9f3464c55639589d25fa6a3ec49c9f238` + await page.getByRole('searchbox', { name: 'Search for addresses, deal' }).fill('0xa201d2c9f3464c55639589d25fa6a3ec49c9f238'); + + // When the search is executed + await page.keyboard.press('Enter'); + + // Then the corresponding entity page is displayed + await expect(page.getByText('App details')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/verify-deal-tabs.spec.ts b/tests/unlogged/verify-deal-tabs.spec.ts new file mode 100644 index 00000000..7dd16b67 --- /dev/null +++ b/tests/unlogged/verify-deal-tabs.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Deal Details Page', () => { + test('Verify deal tabs', async ({ page }) => { + // Navigate directly to a deal details page + await page.goto('http://localhost:5173/arbitrum-mainnet/deal/0x0481dbad29b7863d88a14abb5ed2d57260f464362effd2b063b6970ff3e5b040'); + + // Verify that the DETAILS tab is visible + await expect(page.getByRole('tab', { name: 'DETAILS' })).toBeVisible(); + + // Verify that the TASKS tab is visible + await expect(page.getByRole('tab', { name: 'TASKS' })).toBeVisible(); + + // Verify that the ASSOCIATED DEALS tab is visible + await expect(page.getByRole('tab', { name: 'ASSOCIATED DEALS' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/unlogged/view-all-links.spec.ts b/tests/unlogged/view-all-links.spec.ts new file mode 100644 index 00000000..432821be --- /dev/null +++ b/tests/unlogged/view-all-links.spec.ts @@ -0,0 +1,20 @@ +// spec: specs/plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Index - Search', () => { + test('View all links', async ({ page }) => { + // Navigate to the homepage + await page.goto('http://localhost:5173/arbitrum-mainnet'); + + // Then the links "View all deals / tasks / apps / datasets / workerpools" are accessible on desktop + await expect(page.getByRole('link', { name: 'View all deals' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'View all tasks' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'View all apps' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'View all datasets' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'View all workerpools' })).toBeVisible(); + + // And the "View all" links are accessible on mobile (same links are responsive) + }); +}); \ No newline at end of file