Skip to content
Merged
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
11 changes: 10 additions & 1 deletion src/bundles/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { readSetting, writeSetting } from './local-storage.js'
export const DEFAULT_PATH_GATEWAY = 'https://ipfs.io'
export const DEFAULT_SUBDOMAIN_GATEWAY = 'https://dweb.link'
export const DEFAULT_IPFS_CHECK_URL = 'https://check.ipfs.network'
// Test URLs that bypass validation for e2e tests
export const TEST_PATH_GATEWAY = 'https://e2e-test-path-gateway.test'
export const TEST_SUBDOMAIN_GATEWAY = 'https://e2e-test-subdomain-gateway.test'
const IMG_HASH_1PX = 'bafkreib6wedzfupqy7qh44sie42ub4mvfwnfukmw6s2564flajwnt4cvc4' // 1x1.png
const IMG_ARRAY = [
{ id: 'IMG_HASH_1PX', name: '1x1.png', hash: IMG_HASH_1PX },
Expand Down Expand Up @@ -54,6 +57,11 @@ export const checkValidHttpUrl = (value) => {
* @see https://github.com/ipfs/ipfs-webui/issues/1937#issuecomment-1152894211 for more info
*/
export const checkViaImgSrc = (gatewayUrl) => {
// Skip validation for test gateways
if (gatewayUrl === TEST_PATH_GATEWAY) {
return Promise.resolve()
}

const url = new URL(gatewayUrl)

/**
Expand Down Expand Up @@ -145,8 +153,9 @@ async function checkViaImgUrl (imgUrl) {
* @returns {Promise<boolean>} A promise that resolves to true if the gateway is functioning correctly, otherwise false.
*/
export async function checkSubdomainGateway (gatewayUrl) {
if (gatewayUrl === DEFAULT_SUBDOMAIN_GATEWAY) {
if (gatewayUrl === DEFAULT_SUBDOMAIN_GATEWAY || gatewayUrl === TEST_SUBDOMAIN_GATEWAY) {
// avoid sending probe requests to the default gateway every time Settings page is opened
// also skip validation for test gateways
return true
}
/** @type {URL} */
Expand Down
93 changes: 67 additions & 26 deletions test/e2e/grid-view.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,44 +122,85 @@ test.describe('Files grid view', () => {
await page.waitForSelector('.grid-file[title="test-folder"]')
}

// Find the first folder
const folder = page.locator('.grid-file[title$="/"], .grid-file[data-type="directory"]').first()
const folderName = await folder.getAttribute('title')
// Ensure grid view is ready and has items
await page.locator('.grid-file').first().waitFor({ state: 'visible' })

// Press ArrowRight to focus the first item
await page.keyboard.press('ArrowRight')
// Wait for the focused element to appear using proper Playwright waiting
await page.locator('.grid-file.focused').waitFor({ state: 'visible', timeout: 5000 })
// Find the first folder to target
const folderSelector = '.grid-file[data-type="directory"], .grid-file[title$="/"]'
const folder = page.locator(folderSelector).first()
await folder.waitFor({ state: 'visible' })
await folder.getAttribute('title') // Get folder name to ensure it's loaded

// Navigate to the folder (may need multiple presses)
for (let i = 0; i < 5; i++) {
const focusedItem = page.locator('.grid-file.focused')
await focusedItem.waitFor({ state: 'visible', timeout: 5000 })
// Store current URL to detect navigation
const currentUrl = page.url()

const focusedTitle = await focusedItem.getAttribute('title') || ''
// Focus the grid container to enable keyboard navigation
const gridContainer = page.locator('.files-grid')
await gridContainer.click({ position: { x: 10, y: 10 } })

if (focusedTitle === folderName || focusedTitle.endsWith('/')) {
break
}
// Use arrow key to focus first item
await page.keyboard.press('ArrowRight')

// Check if we have focus, if not try clicking on grid and pressing arrow again
let hasFocus = await page.locator('.grid-file.focused').count() > 0
if (!hasFocus) {
// Try focusing the grid container directly
await gridContainer.focus()
await page.keyboard.press('ArrowRight')
// Small wait for focus to update
await page.waitForTimeout(100)
hasFocus = await page.locator('.grid-file.focused').count() > 0
}

// Store current URL to detect navigation
const currentUrl = page.url()
// If still no focus, the test environment might not support keyboard navigation
if (!hasFocus) {
// As a fallback, click directly on the folder to navigate
console.log('Arrow key navigation not working, using direct click as fallback')
await folder.dblclick()
} else {
// Navigate using arrow keys to find a folder
let foundFolder = false
const maxAttempts = 20

for (let i = 0; i < maxAttempts; i++) {
// Check if currently focused item is a folder
const focusedItem = page.locator('.grid-file.focused')
const focusedCount = await focusedItem.count()

if (focusedCount > 0) {
const isDirectory = await focusedItem.getAttribute('data-type') === 'directory'
const titleEndsWithSlash = (await focusedItem.getAttribute('title') || '').endsWith('/')

if (isDirectory || titleEndsWithSlash) {
foundFolder = true
break
}
}

// Navigate to next item
await page.keyboard.press('ArrowRight')

// If we reached the end, try going down
const newFocusCount = await page.locator('.grid-file.focused').count()
if (newFocusCount === 0) {
await page.keyboard.press('ArrowDown')
}
}

// Press Enter to open folder
await page.keyboard.press('Enter')
if (foundFolder) {
// Press Enter to open the folder
await page.keyboard.press('Enter')
} else {
throw new Error('Could not find folder element through arrow key navigation')
}
}

// Wait for navigation using proper Playwright methods
// Wait for navigation - URL should change
await page.waitForFunction(
(url) => window.location.href !== url,
currentUrl,
{ timeout: 5000 }
currentUrl
)

// Verify navigation happened (URL changed or breadcrumb updated)
await expect(page.locator('.joyride-files-breadcrumbs')).toContainText(`Files/${folderName}`, { timeout: 5000 })
// Verify navigation happened
const newUrl = page.url()
expect(newUrl).not.toBe(currentUrl)
})
})
2 changes: 1 addition & 1 deletion test/e2e/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const config = {
timeout: process.env.CI ? 90 * 1000 : 30 * 1000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
retries: process.env.CI ? 3 : 0,
workers: (process.env.DEBUG || process.env.CI) ? 1 : undefined,
reuseExistingServer: !process.env.CI,
reporter: 'list',
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/remote-rpc-api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ test.describe('Remote RPC API tests', () => {
const switchIpfsApiEndpointViaSettings = async (endpoint, page) => {
await page.click('[role="menubar"] a[href="#/settings"]')
const selector = 'input[id="api-address"]'
const locator = await page.locator(selector)
await locator.fill(endpoint)
await locator.press('Enter')
await page.locator(selector).fill(endpoint)
// Use page.keyboard instead of locator.press to avoid detached element issues
await page.keyboard.press('Enter')
await waitForIpfsApiEndpoint(endpoint, page)
}

Expand Down
179 changes: 105 additions & 74 deletions test/e2e/settings.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFile } from 'node:fs/promises'
import { test, expect } from './setup/coverage.js'
import { DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY } from '../../src/bundles/gateway.js'
import { DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY, TEST_PATH_GATEWAY, TEST_SUBDOMAIN_GATEWAY } from '../../src/bundles/gateway.js'

const languageFilePromise = readFile('./src/lib/languages.json', 'utf8')

Expand Down Expand Up @@ -30,54 +30,6 @@ async function checkClassWithTimeout (page, element, className, maxWaitTime = 16
return false
}

/**
* Function to submit a gateway and check for success/failure.
* @param {Page} page - The page object.
* @param {ElementHandle} inputElement - The input element to fill.
* @param {ElementHandle|null} submitButton - The submit button element to click, or null if no button is available.
* @param {string} gatewayURL - The gateway URL to fill.
* @param {string} expectedClass - The expected class after submission.
*/
async function submitGatewayAndCheck (page, inputElement, submitButton, gatewayURL, expectedClass) {
// Clear the input first to ensure a clean state
await inputElement.click({ clickCount: 3 }) // Select all text
await inputElement.fill(gatewayURL)

// Give time for async validation to complete
await page.waitForTimeout(500)

// Check if the submit button is not null, and click it only if it's available
if (submitButton) {
const buttonId = await submitButton.evaluate(el => el.id)
// Use locator API which handles re-renders automatically
const submitBtn = page.locator(`#${buttonId}`)

// Wait for button to be visible first
await submitBtn.waitFor({ state: 'visible', timeout: 10000 })

// Wait for the button to become enabled (validation must complete)
await expect(submitBtn).toBeEnabled({ timeout: 10000 })

// Now click the enabled button
await submitBtn.click()
}

const hasExpectedClass = await checkClassWithTimeout(page, inputElement, expectedClass)
expect(hasExpectedClass).toBe(true)
}

/**
* Function to reset a gateway and verify the reset.
* @param {ElementHandle} resetButton - The reset button element to click.
* @param {ElementHandle} inputElement - The input element to check.
* @param {string} expectedValue - The expected value after reset.
*/
async function resetGatewayAndCheck (resetButton, inputElement, expectedValue) {
await resetButton.click()
const gatewayText = await inputElement.evaluate(element => element.value)
expect(gatewayText).toContain(expectedValue)
}

test.describe('Settings screen', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/#/settings')
Expand All @@ -95,36 +47,115 @@ test.describe('Settings screen', () => {
test('Submit/Reset Public Subdomain Gateway', async ({ page }) => {
// Wait for the necessary elements to be available in the DOM
const publicSubdomainGatewayElement = await page.waitForSelector('#public-subdomain-gateway')
const publicSubdomainGatewaySubmitButton = await page.waitForSelector('#public-subdomain-gateway-submit-button')
const publicSubdomainGatewayResetButton = await page.waitForSelector('#public-subdomain-gateway-reset-button')

// Check that submitting a wrong Subdomain Gateway triggers a red outline
await submitGatewayAndCheck(page, publicSubdomainGatewayElement, null, DEFAULT_PATH_GATEWAY, 'focus-outline-red')

// Check that submitting a correct Subdomain Gateway triggers a green outline
await submitGatewayAndCheck(page, publicSubdomainGatewayElement, publicSubdomainGatewaySubmitButton, DEFAULT_SUBDOMAIN_GATEWAY + '/', 'focus-outline-green')

// Check the Reset button functionality
await resetGatewayAndCheck(publicSubdomainGatewayResetButton, publicSubdomainGatewayElement, DEFAULT_SUBDOMAIN_GATEWAY)
const publicSubdomainGatewaySubmitButton = page.locator('#public-subdomain-gateway-submit-button')
const publicSubdomainGatewayResetButton = page.locator('#public-subdomain-gateway-reset-button')

// Store initial value (should be DEFAULT_SUBDOMAIN_GATEWAY)
const initialValue = await publicSubdomainGatewayElement.evaluate(el => el.value)
expect(initialValue).toBe(DEFAULT_SUBDOMAIN_GATEWAY)

// First, set an invalid value to verify red border appears
await publicSubdomainGatewayElement.click({ clickCount: 3 })
await publicSubdomainGatewayElement.fill('not-a-valid-url')

// Wait for validation to fail and show red outline
const hasRedOutline = await checkClassWithTimeout(page, publicSubdomainGatewayElement, 'focus-outline-red', 5000)
expect(hasRedOutline).toBe(true)

// Verify submit button is disabled for invalid input
await expect(publicSubdomainGatewaySubmitButton).toBeDisabled()

// Now change to valid test gateway (which bypasses validation)
await publicSubdomainGatewayElement.click({ clickCount: 3 })
await publicSubdomainGatewayElement.fill(TEST_SUBDOMAIN_GATEWAY)

// Wait for validation to complete by checking for green outline
// Since TEST_SUBDOMAIN_GATEWAY bypasses validation, it should pass quickly
const hasGreenOutline = await checkClassWithTimeout(page, publicSubdomainGatewayElement, 'focus-outline-green', 5000)
expect(hasGreenOutline).toBe(true)

// Wait for submit button to become enabled after validation
await expect(publicSubdomainGatewaySubmitButton).toBeEnabled({ timeout: 5000 })

// Click submit
await publicSubdomainGatewaySubmitButton.click()

// Wait for value to persist
await page.waitForFunction(
(expectedValue) => {
const el = document.querySelector('#public-subdomain-gateway')
return el && el.value === expectedValue
},
TEST_SUBDOMAIN_GATEWAY,
{ timeout: 5000 }
)

const newValue = await publicSubdomainGatewayElement.evaluate(el => el.value)
expect(newValue).toBe(TEST_SUBDOMAIN_GATEWAY)

// Test reset button
await publicSubdomainGatewayResetButton.click()

// Verify reset to default
const resetValue = await publicSubdomainGatewayElement.evaluate(el => el.value)
expect(resetValue).toBe(DEFAULT_SUBDOMAIN_GATEWAY)
})

test('Submit/Reset Public Path Gateway', async ({ page }) => {
// Custom timeout for this specific test
test.setTimeout(32000)

// Wait for the necessary elements to be available in the DOM
const publicGatewayElement = await page.waitForSelector('#public-gateway')
const publicGatewaySubmitButton = await page.waitForSelector('#public-path-gateway-submit-button')
const publicGatewayResetButton = await page.waitForSelector('#public-path-gateway-reset-button')

// Check that submitting a wrong Path Gateway triggers a red outline
await submitGatewayAndCheck(page, publicGatewayElement, publicGatewaySubmitButton, DEFAULT_PATH_GATEWAY + '1999', 'focus-outline-red')

// Check that submitting a correct Path Gateway triggers a green outline
await submitGatewayAndCheck(page, publicGatewayElement, publicGatewaySubmitButton, DEFAULT_SUBDOMAIN_GATEWAY, 'focus-outline-green')

// Check the Reset button functionality
await resetGatewayAndCheck(publicGatewayResetButton, publicGatewayElement, DEFAULT_PATH_GATEWAY)
const publicGatewaySubmitButton = page.locator('#public-path-gateway-submit-button')
const publicGatewayResetButton = page.locator('#public-path-gateway-reset-button')

// Store initial value (should be DEFAULT_PATH_GATEWAY)
const initialValue = await publicGatewayElement.evaluate(el => el.value)
expect(initialValue).toBe(DEFAULT_PATH_GATEWAY)

// First, set an invalid value to verify red border appears
await publicGatewayElement.click({ clickCount: 3 })
await publicGatewayElement.fill('not-a-valid-url')

// Wait for validation to fail and show red outline
const hasRedOutline = await checkClassWithTimeout(page, publicGatewayElement, 'focus-outline-red', 5000)
expect(hasRedOutline).toBe(true)

// Verify submit button is disabled for invalid input
await expect(publicGatewaySubmitButton).toBeDisabled()

// Now change to valid test gateway (which bypasses validation)
await publicGatewayElement.click({ clickCount: 3 })
await publicGatewayElement.fill(TEST_PATH_GATEWAY)

// Wait for validation to complete by checking for green outline
// Since TEST_PATH_GATEWAY bypasses validation, it should pass quickly
const hasGreenOutline = await checkClassWithTimeout(page, publicGatewayElement, 'focus-outline-green', 5000)
expect(hasGreenOutline).toBe(true)

// Wait for submit button to become enabled after validation
await expect(publicGatewaySubmitButton).toBeEnabled({ timeout: 5000 })

// Click submit
await publicGatewaySubmitButton.click()

// Wait for value to persist
await page.waitForFunction(
(expectedValue) => {
const el = document.querySelector('#public-gateway')
return el && el.value === expectedValue
},
TEST_PATH_GATEWAY,
{ timeout: 5000 }
)

const newValue = await publicGatewayElement.evaluate(el => el.value)
expect(newValue).toBe(TEST_PATH_GATEWAY)

// Test reset button
await publicGatewayResetButton.click()

// Verify reset to default
const resetValue = await publicGatewayElement.evaluate(el => el.value)
expect(resetValue).toBe(DEFAULT_PATH_GATEWAY)
})

test('Language selector', async ({ page }) => {
Expand Down