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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitForUrl } from './utils';
import { waitForUrl, waitForElementText } from './utils';

describe('browser back/forward buttons', () => {
it('works on a single page with InstantSearch', async () => {
Expand Down Expand Up @@ -74,11 +74,8 @@ describe('browser back/forward buttons', () => {
await audioLink.click();
await waitForUrl('http://localhost:3000/Audio');

// wait a bit for results
await browser.pause(1000);

firstHit = await (await $('.ais-Hits-item')).getText();
expect(firstHit).toContain('Apple - EarPods');
// Wait for results to load by checking for specific content
await waitForElementText('.ais-Hits-item', 'Apple - EarPods');

await browser.back();
await waitForUrl('http://localhost:3000/Appliances');
Expand All @@ -97,11 +94,17 @@ describe('browser back/forward buttons', () => {
await audioLink.click();
await waitForUrl('http://localhost:3000/');

// wait a bit for results
await browser.pause(1000);

firstHit = await (await $('.ais-Hits-item')).getText();
expect(firstHit).toContain('Amazon');
// Wait for results to load by checking for specific content
await browser.waitUntil(
async () => {
const hitText = await (await $('.ais-Hits-item')).getText();
return hitText.includes('Amazon');
},
{
timeout: 15000,
timeoutMsg: 'Expected first hit to contain "Amazon"',
}
);

await browser.back();
await waitForUrl('http://localhost:3000/Appliances');
Expand Down
104 changes: 102 additions & 2 deletions packages/react-instantsearch-nextjs/__tests__/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,103 @@
export async function waitForUrl(url: string) {
return await browser.waitUntil(async () => (await browser.getUrl()) === url);
export async function waitForUrl(url: string, timeout = 15000) {
return await browser.waitUntil(
async () => {
try {
const currentUrl = await browser.getUrl();
return currentUrl === url;
} catch (error) {
// Retry on network errors
console.log(`Error getting URL: ${error.message}`);
return false;
}
},
{
timeout,
timeoutMsg: `Expected URL to be "${url}" but got "${await browser.getUrl()}" after ${timeout}ms`,
interval: 500,
}
);
}

export async function waitForElementText(selector: string, expectedText: string, timeout = 15000) {
return await browser.waitUntil(
async () => {
try {
const element = await $(selector);

Check warning on line 25 in packages/react-instantsearch-nextjs/__tests__/e2e/utils.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/react-instantsearch-nextjs/__tests__/e2e/utils.ts#L25

User controlled data in a `$(...)` is an anti-pattern that can lead to XSS vulnerabilities
const text = await element.getText();
return text === expectedText;
} catch (error) {
console.log(`Error getting text from ${selector}: ${error.message}`);
return false;
}
},
{
timeout,
timeoutMsg: `Expected element "${selector}" to have text "${expectedText}" after ${timeout}ms`,
interval: 500,
}
);
}

export async function waitForInputValue(selector: string, expectedValue: string, timeout = 15000) {
return await browser.waitUntil(
async () => {
try {
const element = await $(selector);
const value = await element.getValue();
return value === expectedValue;
} catch (error) {
console.log(`Error getting value from ${selector}: ${error.message}`);
return false;
}
},
{
timeout,
timeoutMsg: `Expected input "${selector}" to have value "${expectedValue}" after ${timeout}ms`,
interval: 500,
}
);
}

export async function waitForElementAttribute(selector: string, attribute: string, expectedValue: string, timeout = 15000) {
return await browser.waitUntil(
async () => {
try {
const element = await $(selector);
const value = await element.getAttribute(attribute);
return value === expectedValue;
} catch (error) {
console.log(`Error getting attribute ${attribute} from ${selector}: ${error.message}`);
return false;
}
},
{
timeout,
timeoutMsg: `Expected element "${selector}" to have attribute "${attribute}" with value "${expectedValue}" after ${timeout}ms`,
interval: 500,
}
);
}

/**
* Wait for page to be ready by checking for document readiness and no pending navigation
*/
export async function waitForPageReady(timeout = 10000) {
return await browser.waitUntil(
async () => {
try {
const ready = await browser.execute(() => {
return document.readyState === 'complete' && !document.hidden;
});
return ready;
} catch (error) {
console.log(`Error checking page readiness: ${error.message}`);
return false;
}
},
{
timeout,
timeoutMsg: `Page did not become ready after ${timeout}ms`,
interval: 200,
}
);
}
4 changes: 2 additions & 2 deletions packages/react-instantsearch-nextjs/wdio.conf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports.config = {
logLevel: 'info',
bail: 1,
baseUrl: 'http://localhost:3000',
waitforTimeout: 10000,
waitforTimeout: 15000,
services: ['selenium-standalone'],
seleniumInstallArgs: { drivers: { chrome: { version: '2.43' } } },
capabilities: [
Expand All @@ -18,7 +18,7 @@ exports.config = {
reporters: ['spec'],

jasmineNodeOpts: {
defaultTimeoutInterval: 60000,
defaultTimeoutInterval: 90000,
},

before() {
Expand Down
6 changes: 3 additions & 3 deletions packages/react-instantsearch-nextjs/wdio.saucelabs.conf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ exports.config = {
*/
sauceConnectOpts: {
/*
* Retry to establish a tunnel 2 times maximum on fail
* Retry to establish a tunnel 3 times maximum on fail
* This is useful to prevent premature test failure if we have difficulties to open the tunnel
* (can happen if there are already multiple tunnels opened on SauceLabs)
* https://github.com/bermi/sauce-connect-launcher#advanced-usage
*/
connectRetries: 2,
connectRetryTimeout: 10000,
connectRetries: 3,
connectRetryTimeout: 15000,
},
/*
* Sauce Labs Open Source offer has a maximum of 5 concurrent session
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitForUrl } from './utils';
import { waitForUrl, waitForElementAttribute } from './utils';

describe('browser back/forward buttons works on routes pushed by InstantSearch', () => {
it('works when not on a i18n route', async () => {
Expand Down Expand Up @@ -28,8 +28,8 @@ describe('browser back/forward buttons works on routes pushed by InstantSearch',

await waitForUrl(urlWithRefinement);

const checkbox = await $('.ais-RefinementList-checkbox[value="Apple"]');
expect(await checkbox.getAttribute('checked')).toBe('true');
// Wait for the checkbox to be checked, which ensures the state has been restored
await waitForElementAttribute('.ais-RefinementList-checkbox[value="Apple"]', 'checked', 'true');
});

it('works when on a i18n route', async () => {
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('browser back/forward buttons works on routes pushed by InstantSearch',

await waitForUrl(urlWithRefinement);

const checkbox = await $('.ais-RefinementList-checkbox[value="Apple"]');
expect(await checkbox.getAttribute('checked')).toBe('true');
// Wait for the checkbox to be checked, which ensures the state has been restored
await waitForElementAttribute('.ais-RefinementList-checkbox[value="Apple"]', 'checked', 'true');
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitForUrl } from './utils';
import { waitForUrl, waitForInputValue } from './utils';

describe('clicking on a Next.js link within the same page updates InstantSearch', () => {
it('works when not on a i18n route', async () => {
Expand All @@ -9,13 +9,8 @@ describe('clicking on a Next.js link within the same page updates InstantSearch'

await waitForUrl('http://localhost:3000/?instant_search%5Bquery%5D=apple');

const searchInput = await $('.ais-SearchBox-input');

expect(
await browser.waitUntil(async () => {
return (await searchInput.getValue()) === 'apple';
})
).toBe(true);
// Wait for the input value to be updated, which ensures the page has fully loaded
await waitForInputValue('.ais-SearchBox-input', 'apple');
});

it('works when on a i18n route', async () => {
Expand All @@ -28,11 +23,7 @@ describe('clicking on a Next.js link within the same page updates InstantSearch'
'http://localhost:3000/fr?instant_search%5Bquery%5D=apple'
);

const searchInput = await $('.ais-SearchBox-input');
expect(
await browser.waitUntil(async () => {
return (await searchInput.getValue()) === 'apple';
})
).toBe(true);
// Wait for the input value to be updated, which ensures the page has fully loaded
await waitForInputValue('.ais-SearchBox-input', 'apple');
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitForUrl } from './utils';
import { waitForUrl, waitForInputValue, waitForElementText } from './utils';

describe('refining InstantSearch causes only one onStateChange', () => {
describe('Next.js Link', () => {
Expand All @@ -12,13 +12,11 @@ describe('refining InstantSearch causes only one onStateChange', () => {
'http://localhost:3000/test?instant_search%5Bquery%5D=apple'
);

const searchInput = await $('.ais-SearchBox-input');
await browser.waitUntil(async () => {
return (await searchInput.getValue()) === 'apple';
});

const output = await $('output#onStateChange');
expect(await output.getText()).toBe('1');
// Wait for both URL and input value to be consistent
await waitForInputValue('.ais-SearchBox-input', 'apple');

// Wait for the onStateChange counter to be updated
await waitForElementText('output#onStateChange', '1');
});

it('works when on a i18n route', async () => {
Expand All @@ -31,13 +29,11 @@ describe('refining InstantSearch causes only one onStateChange', () => {
'http://localhost:3000/fr/test?instant_search%5Bquery%5D=apple'
);

const searchInput = await $('.ais-SearchBox-input');
await browser.waitUntil(async () => {
return (await searchInput.getValue()) === 'apple';
});

const output = await $('output#onStateChange');
expect(await output.getText()).toBe('1');
// Wait for both URL and input value to be consistent
await waitForInputValue('.ais-SearchBox-input', 'apple');

// Wait for the onStateChange counter to be updated
await waitForElementText('output#onStateChange', '1');
});
});

Expand All @@ -52,8 +48,8 @@ describe('refining InstantSearch causes only one onStateChange', () => {
'http://localhost:3000/test?instant_search%5BrefinementList%5D%5Bbrand%5D%5B0%5D=Apple'
);

const output = await $('output#onStateChange');
expect(await output.getText()).toBe('1');
// Wait for the onStateChange counter to be updated
await waitForElementText('output#onStateChange', '1');
});

it('works when on a i18n route', async () => {
Expand All @@ -66,8 +62,8 @@ describe('refining InstantSearch causes only one onStateChange', () => {
'http://localhost:3000/fr/test?instant_search%5BrefinementList%5D%5Bbrand%5D%5B0%5D=Apple'
);

const output = await $('output#onStateChange');
expect(await output.getValue()).toBe('1');
// Wait for the onStateChange counter to be updated
await waitForElementText('output#onStateChange', '1');
});
});
});
Loading