From 53e9983d83d0bcdc1d84617d4fadf0707d9dfbb5 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 21 Jan 2020 14:33:58 -0600 Subject: [PATCH] Add hydration callback for testing (#10195) * Add hydration callback for testing * Update size-limit test --- packages/next/client/index.js | 8 + .../auto-export/test/index.test.js | 4 - test/integration/chunking/test/index.test.js | 10 +- test/integration/config/test/client.js | 2 - .../test/index.test.js | 2 - .../css-client-nav/test/index.test.js | 4 - .../css-modules/test/index.test.js | 1 - test/integration/css/test/index.test.js | 3 - .../duplicate-pages/test/index.test.js | 2 - .../dynamic-routing/test/index.test.js | 4 - .../test/index.test.js | 1 - .../test/index.test.js | 2 - .../export-dynamic-pages/test/index.test.js | 2 - .../export-serverless/test/browser.js | 3 +- test/integration/export/test/browser.js | 3 +- test/integration/future/test/index.test.js | 2 - test/integration/hydration/test/index.test.js | 9 +- .../initial-ref/test/index.test.js | 4 +- .../invalid-href/test/index.test.js | 1 - test/integration/link-ref/test/index.test.js | 1 - .../next-dynamic/test/index.test.js | 2 - .../next-plugins/test/index.test.js | 3 - test/integration/polyfills/test/index.test.js | 9 +- .../preload-viewport/test/index.test.js | 4 - test/integration/prerender/test/index.test.js | 5 - .../integration/production/test/index.test.js | 1 - test/integration/production/test/security.js | 4 +- .../integration/size-limit/test/index.test.js | 2 +- test/lib/next-test-utils.js | 7 +- test/lib/next-webdriver.d.ts | 28 ++++ test/lib/next-webdriver.js | 150 +++--------------- test/lib/wd-chain.js | 133 ++++++++++++++++ test/tsconfig.json | 9 ++ 33 files changed, 217 insertions(+), 208 deletions(-) create mode 100644 test/lib/next-webdriver.d.ts create mode 100644 test/lib/wd-chain.js create mode 100644 test/tsconfig.json diff --git a/packages/next/client/index.js b/packages/next/client/index.js index 4650c74e1af3f24..bbbb9f60732ae85 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -114,6 +114,14 @@ class Container extends React.Component { } ) } + + if (process.env.__NEXT_TEST_MODE) { + window.__NEXT_HYDRATED = true + + if (window.__NEXT_HYDRATED_CB) { + window.__NEXT_HYDRATED_CB() + } + } } componentDidUpdate() { diff --git a/test/integration/auto-export/test/index.test.js b/test/integration/auto-export/test/index.test.js index 0b1d30fa66a12ad..a7216042c7398e7 100644 --- a/test/integration/auto-export/test/index.test.js +++ b/test/integration/auto-export/test/index.test.js @@ -8,7 +8,6 @@ import { findPort, killApp, launchApp, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1 @@ -42,14 +41,12 @@ const runTests = () => { it('should update asPath after mount', async () => { const browser = await webdriver(appPort, '/zeit/cmnt-2') - await waitFor(500) const html = await browser.eval(`document.documentElement.innerHTML`) expect(html).toMatch(/\/zeit\/cmnt-2/) }) it('should not replace URL with page name while asPath is delayed', async () => { const browser = await webdriver(appPort, '/zeit/cmnt-1') - await waitFor(500) const val = await browser.eval(`!!window.pathnames.find(function(p) { return p !== '/zeit/cmnt-1' })`) @@ -84,7 +81,6 @@ describe('Auto Export', () => { it('should not show hydration warning from mismatching asPath', async () => { const browser = await webdriver(appPort, '/zeit/cmnt-1') - await waitFor(500) const numCaught = await browser.eval(`window.caughtWarns.length`) expect(numCaught).toBe(0) diff --git a/test/integration/chunking/test/index.test.js b/test/integration/chunking/test/index.test.js index 01a32228f705e14..f5563a6b554ef72 100644 --- a/test/integration/chunking/test/index.test.js +++ b/test/integration/chunking/test/index.test.js @@ -3,13 +3,7 @@ import { join } from 'path' import express from 'express' import http from 'http' -import { - nextBuild, - waitFor, - nextServer, - promiseCall, - stopApp, -} from 'next-test-utils' +import { nextBuild, nextServer, promiseCall, stopApp } from 'next-test-utils' import { readdir, readFile, unlink, access } from 'fs-extra' import cheerio from 'cheerio' import webdriver from 'next-webdriver' @@ -134,7 +128,6 @@ describe('Chunking', () => { it('should hydrate with granularChunks config', async () => { const browser = await webdriver(appPort, '/page2') - await waitFor(1000) const text = await browser.elementByCss('#padded-str').text() expect(text).toBe('__rad__') @@ -144,7 +137,6 @@ describe('Chunking', () => { it('should load chunks when navigating', async () => { const browser = await webdriver(appPort, '/page3') - await waitFor(1000) const text = await browser .elementByCss('#page2-link') .click() diff --git a/test/integration/config/test/client.js b/test/integration/config/test/client.js index 9d09666a7a0f0ec..5181256e2381e73 100644 --- a/test/integration/config/test/client.js +++ b/test/integration/config/test/client.js @@ -8,8 +8,6 @@ export default (context, render) => { describe('Configuration', () => { it('should have config available on the client', async () => { const browser = await webdriver(context.appPort, '/next-config') - // Wait for client side to load - await waitFor(10000) const serverText = await browser.elementByCss('#server-only').text() const serverClientText = await browser diff --git a/test/integration/conflicting-public-file-page/test/index.test.js b/test/integration/conflicting-public-file-page/test/index.test.js index cb5a7ee3c0d426b..2ac948bb92f214f 100644 --- a/test/integration/conflicting-public-file-page/test/index.test.js +++ b/test/integration/conflicting-public-file-page/test/index.test.js @@ -7,7 +7,6 @@ import { findPort, killApp, renderViaHTTP, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5 @@ -25,7 +24,6 @@ describe('Errors on conflict between public file and page file', () => { /A conflicting public file and page file was found for path/ ) } - await waitFor(1000) await killApp(app) }) diff --git a/test/integration/css-client-nav/test/index.test.js b/test/integration/css-client-nav/test/index.test.js index 3fdbcf58db2c157..d5dfc8cba184a0a 100644 --- a/test/integration/css-client-nav/test/index.test.js +++ b/test/integration/css-client-nav/test/index.test.js @@ -5,7 +5,6 @@ import { remove } from 'fs-extra' import { nextBuild, nextStart, - waitFor, findPort, killApp, renderViaHTTP, @@ -80,9 +79,6 @@ describe('CSS Module client-side navigation in Production', () => { let browser try { browser = await webdriver(appPort, '/blue') - - await waitFor(2000) // Ensure hydration - await browser.eval(`window.__did_not_ssr = 'make sure this is set'`) const redColor = await browser.eval( diff --git a/test/integration/css-modules/test/index.test.js b/test/integration/css-modules/test/index.test.js index cb2eb58aeff4e56..3c0d865fd44dc09 100644 --- a/test/integration/css-modules/test/index.test.js +++ b/test/integration/css-modules/test/index.test.js @@ -178,7 +178,6 @@ xdescribe('Can hot reload CSS Module without losing state', () => { // FIXME: this is broken it('should update CSS color without remounting ', async () => { const browser = await webdriver(appPort, '/') - await waitFor(2000) // ensure application hydrates const desiredText = 'hello world' await browser.elementById('text-input').type(desiredText) diff --git a/test/integration/css/test/index.test.js b/test/integration/css/test/index.test.js index 8d2b401e62a54e0..b4a971c74c934d9 100644 --- a/test/integration/css/test/index.test.js +++ b/test/integration/css/test/index.test.js @@ -304,7 +304,6 @@ describe('CSS Support', () => { let browser try { browser = await webdriver(appPort, '/page1') - await waitFor(2000) // ensure application hydrates const desiredText = 'hello world' await browser.elementById('text-input').type(desiredText) @@ -703,7 +702,6 @@ describe('CSS Support', () => { it('should have the correct color (css ordering)', async () => { const browser = await webdriver(appPort, '/') - await waitFor(2000) // ensure application hydrates const currentColor = await browser.eval( `window.getComputedStyle(document.querySelector('.my-text')).color` @@ -729,7 +727,6 @@ describe('CSS Support', () => { it('should have the correct color (css ordering)', async () => { const browser = await webdriver(appPort, '/') - await waitFor(2000) // ensure application hydrates const currentColor = await browser.eval( `window.getComputedStyle(document.querySelector('.my-text')).color` diff --git a/test/integration/duplicate-pages/test/index.test.js b/test/integration/duplicate-pages/test/index.test.js index 2b2b50f5e411996..cbcd06fffcd6d0f 100644 --- a/test/integration/duplicate-pages/test/index.test.js +++ b/test/integration/duplicate-pages/test/index.test.js @@ -8,7 +8,6 @@ import { launchApp, renderViaHTTP, killApp, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1 @@ -34,7 +33,6 @@ describe('Handles Duplicate Pages', () => { onStderr: handleOutput, }) await renderViaHTTP(appPort, '/hello') - await waitFor(3000) await killApp(app) expect(output).toMatch(/Duplicate page detected/) }) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 643c6407b82a322..4edbcae89075d77 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -304,7 +304,6 @@ function runTests(dev) { expect(html).toMatch(/onmpost:.*pending/) const browser = await webdriver(appPort, '/on-mount/post-1') - await waitFor(1000) const text = await browser.eval(`document.body.innerHTML`) expect(text).toMatch(/onmpost:.*post-1/) }) @@ -316,14 +315,12 @@ function runTests(dev) { it('should update with a hash in the URL', async () => { const browser = await webdriver(appPort, '/on-mount/post-1#abc') - await waitFor(1000) const text = await browser.eval(`document.body.innerHTML`) expect(text).toMatch(/onmpost:.*post-1/) }) it('should scroll to a hash on mount', async () => { const browser = await webdriver(appPort, '/on-mount/post-1#item-400') - await waitFor(1000) const text = await browser.eval(`document.body.innerHTML`) expect(text).toMatch(/onmpost:.*post-1/) @@ -334,7 +331,6 @@ function runTests(dev) { it('should scroll to a hash on client-side navigation', async () => { const browser = await webdriver(appPort, '/') - await waitFor(1000) await browser.elementByCss('#view-dynamic-with-hash').click() await browser.waitForElementByCss('p') diff --git a/test/integration/empty-object-getInitialProps/test/index.test.js b/test/integration/empty-object-getInitialProps/test/index.test.js index 951969217a548c8..e3773336850864d 100644 --- a/test/integration/empty-object-getInitialProps/test/index.test.js +++ b/test/integration/empty-object-getInitialProps/test/index.test.js @@ -49,7 +49,6 @@ describe('Empty Project', () => { it('should show empty object warning during client transition', async () => { const browser = await webdriver(appPort, '/static') - await waitFor(1000) await browser.eval(`(function() { window.gotWarn = false const origWarn = console.warn diff --git a/test/integration/export-dynamic-pages-serverless/test/index.test.js b/test/integration/export-dynamic-pages-serverless/test/index.test.js index dd2c576ee0ae301..c7fc27ac4462ae2 100644 --- a/test/integration/export-dynamic-pages-serverless/test/index.test.js +++ b/test/integration/export-dynamic-pages-serverless/test/index.test.js @@ -9,7 +9,6 @@ import { startCleanStaticServer, stopApp, renderViaHTTP, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 @@ -41,7 +40,6 @@ describe('Export Dyanmic Pages', () => { expect.assertions(1) const browser = await webdriver(port, '/regression/jeff-is-cool') try { - await waitFor(3000) expect(await browser.eval(`window.__AS_PATHS`)).toEqual([ '/regression/jeff-is-cool', ]) diff --git a/test/integration/export-dynamic-pages/test/index.test.js b/test/integration/export-dynamic-pages/test/index.test.js index dd2c576ee0ae301..c7fc27ac4462ae2 100644 --- a/test/integration/export-dynamic-pages/test/index.test.js +++ b/test/integration/export-dynamic-pages/test/index.test.js @@ -9,7 +9,6 @@ import { startCleanStaticServer, stopApp, renderViaHTTP, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 @@ -41,7 +40,6 @@ describe('Export Dyanmic Pages', () => { expect.assertions(1) const browser = await webdriver(port, '/regression/jeff-is-cool') try { - await waitFor(3000) expect(await browser.eval(`window.__AS_PATHS`)).toEqual([ '/regression/jeff-is-cool', ]) diff --git a/test/integration/export-serverless/test/browser.js b/test/integration/export-serverless/test/browser.js index c62984d088c438e..8a73de458198d61 100644 --- a/test/integration/export-serverless/test/browser.js +++ b/test/integration/export-serverless/test/browser.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import webdriver from 'next-webdriver' -import { check, waitFor, getBrowserBodyText } from 'next-test-utils' +import { check, getBrowserBodyText } from 'next-test-utils' export default function(context) { describe('Render via browser', () => { @@ -184,7 +184,6 @@ export default function(context) { it('should update query after mount', async () => { const browser = await webdriver(context.port, '/query-update?hello=world') - await waitFor(2000) const query = await browser.elementByCss('#query').text() expect(JSON.parse(query)).toEqual({ hello: 'world', a: 'blue' }) await browser.close() diff --git a/test/integration/export/test/browser.js b/test/integration/export/test/browser.js index 201634536fe6475..92ea5604a50c248 100644 --- a/test/integration/export/test/browser.js +++ b/test/integration/export/test/browser.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import webdriver from 'next-webdriver' -import { check, waitFor, getBrowserBodyText } from 'next-test-utils' +import { check, getBrowserBodyText } from 'next-test-utils' export default function(context) { describe('Render via browser', () => { @@ -191,7 +191,6 @@ export default function(context) { it('should update query after mount', async () => { const browser = await webdriver(context.port, '/query-update?hello=world') - await waitFor(2000) const query = await browser.elementByCss('#query').text() expect(JSON.parse(query)).toEqual({ hello: 'world', a: 'blue' }) await browser.close() diff --git a/test/integration/future/test/index.test.js b/test/integration/future/test/index.test.js index 3878b8d8e77d233..8988c97205a5411 100644 --- a/test/integration/future/test/index.test.js +++ b/test/integration/future/test/index.test.js @@ -8,7 +8,6 @@ import { startApp, stopApp, renderViaHTTP, - waitFor, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5 @@ -34,7 +33,6 @@ describe('future.excludeDefaultMomentLocales', () => { it('should load momentjs', async () => { const browser = await webdriver(appPort, '/') - await waitFor(5000) expect(await browser.elementByCss('h1').text()).toMatch(/current time/i) const locales = await browser.eval('moment.locales()') expect(locales).toEqual(['en']) diff --git a/test/integration/hydration/test/index.test.js b/test/integration/hydration/test/index.test.js index 5a18abec58403b1..bca7263bf2feff6 100644 --- a/test/integration/hydration/test/index.test.js +++ b/test/integration/hydration/test/index.test.js @@ -3,13 +3,7 @@ import path from 'path' import fs from 'fs-extra' import webdriver from 'next-webdriver' -import { - nextBuild, - nextStart, - findPort, - waitFor, - killApp, -} from 'next-test-utils' +import { nextBuild, nextStart, findPort, killApp } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1 const appDir = path.join(__dirname, '..') @@ -27,7 +21,6 @@ describe('Hydration', () => { it('Hydrates correctly', async () => { const browser = await webdriver(appPort, '/') - await waitFor(2000) expect(await browser.eval('window.didHydrate')).toBe(true) }) }) diff --git a/test/integration/initial-ref/test/index.test.js b/test/integration/initial-ref/test/index.test.js index a418167f7db847b..6ec48eddd025ef9 100644 --- a/test/integration/initial-ref/test/index.test.js +++ b/test/integration/initial-ref/test/index.test.js @@ -7,7 +7,6 @@ import { nextStart, launchApp, findPort, - waitFor, killApp, } from 'next-test-utils' @@ -19,12 +18,11 @@ let appPort const runTest = () => { it('Has correct initial ref values', async () => { const browser = await webdriver(appPort, '/') - await waitFor(2000) expect(await browser.elementByCss('#ref-val').text()).toContain('76px') }) } -describe('Hydration', () => { +describe('Initial Refs', () => { describe('production mode', () => { beforeAll(async () => { await nextBuild(appDir) diff --git a/test/integration/invalid-href/test/index.test.js b/test/integration/invalid-href/test/index.test.js index 0bd3d46fb917c91..8382ed9b43b95fc 100644 --- a/test/integration/invalid-href/test/index.test.js +++ b/test/integration/invalid-href/test/index.test.js @@ -64,7 +64,6 @@ const showsError = async ( const noError = async (pathname, click = false) => { const browser = await webdriver(appPort, '/') - await waitFor(2000) await browser.eval(`(function() { window.caughtErrors = [] window.addEventListener('error', function (error) { diff --git a/test/integration/link-ref/test/index.test.js b/test/integration/link-ref/test/index.test.js index d51489091a8cc84..cab64da4cafc8bc 100644 --- a/test/integration/link-ref/test/index.test.js +++ b/test/integration/link-ref/test/index.test.js @@ -35,7 +35,6 @@ const noError = async pathname => { const didPrefetch = async pathname => { const browser = await webdriver(appPort, pathname) - await waitFor(500) const links = await browser.elementsByCss('link[rel=prefetch]') let found = false diff --git a/test/integration/next-dynamic/test/index.test.js b/test/integration/next-dynamic/test/index.test.js index 6e16fcee311ab13..01db1ae88fd498d 100644 --- a/test/integration/next-dynamic/test/index.test.js +++ b/test/integration/next-dynamic/test/index.test.js @@ -7,7 +7,6 @@ import { findPort, launchApp, killApp, - waitFor, runNextCommand, nextServer, startApp, @@ -29,7 +28,6 @@ function runTests() { it('should render dynamic server rendered values on client mount', async () => { const browser = await webdriver(appPort, '/') - await waitFor(5000) const text = await browser.elementByCss('#first-render').text() // Failure case is 'Index3' diff --git a/test/integration/next-plugins/test/index.test.js b/test/integration/next-plugins/test/index.test.js index dcedcfd0c998ed6..2024a179ad58116 100644 --- a/test/integration/next-plugins/test/index.test.js +++ b/test/integration/next-plugins/test/index.test.js @@ -8,7 +8,6 @@ import { findPort, launchApp, killApp, - waitFor, nextBuild, nextStart, renderViaHTTP, @@ -57,7 +56,6 @@ function runTests() { it('should call clientInit from plugin correctly', async () => { const browser = await webdriver(appPort, '/') - await waitFor(250) expect(await browser.eval('window.didClientInit')).toBe(true) }) @@ -125,7 +123,6 @@ describe('Next.js plugins', () => { it('should expose a plugins config', async () => { const browser = await webdriver(appPort, '/') - await waitFor(500) expect(await browser.eval('window.initClientConfig')).toBe('world') }) }) diff --git a/test/integration/polyfills/test/index.test.js b/test/integration/polyfills/test/index.test.js index 27e404a6770baf2..d14af807461f80b 100644 --- a/test/integration/polyfills/test/index.test.js +++ b/test/integration/polyfills/test/index.test.js @@ -1,13 +1,7 @@ /* eslint-env jest */ /* global jasmine */ import { join } from 'path' -import { - nextBuild, - findPort, - waitFor, - nextStart, - killApp, -} from 'next-test-utils' +import { nextBuild, findPort, nextStart, killApp } from 'next-test-utils' import webdriver from 'next-webdriver' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1 @@ -34,7 +28,6 @@ describe('Polyfills', () => { it('should alias fetch', async () => { const browser = await webdriver(appPort, '/fetch') - await waitFor(1000) const text = await browser.elementByCss('#test-status').text() expect(text).toBe('pass') diff --git a/test/integration/preload-viewport/test/index.test.js b/test/integration/preload-viewport/test/index.test.js index 8b49bcf85bb1f1c..96e246fe0f871f4 100644 --- a/test/integration/preload-viewport/test/index.test.js +++ b/test/integration/preload-viewport/test/index.test.js @@ -36,7 +36,6 @@ describe('Prefetching Links in viewport', () => { let browser try { browser = await webdriver(appPort, '/') - await waitFor(2 * 1000) const links = await browser.elementsByCss('link[rel=prefetch]') let found = false @@ -124,7 +123,6 @@ describe('Prefetching Links in viewport', () => { it('should not prefetch when prefetch is explicitly set to false', async () => { const browser = await webdriver(appPort, '/opt-out') - await waitFor(2 * 1000) const links = await browser.elementsByCss('link[rel=prefetch]') let found = false @@ -141,7 +139,6 @@ describe('Prefetching Links in viewport', () => { it('should not duplicate prefetches', async () => { const browser = await webdriver(appPort, '/multi-prefetch') - await waitFor(2 * 1000) const links = await browser.elementsByCss('link[rel=prefetch]') @@ -163,7 +160,6 @@ describe('Prefetching Links in viewport', () => { // info: both `/` and `/de-duped` ref the `/first` page, which we don't // want to be re-fetched/re-observed. const browser = await webdriver(appPort, '/') - await waitFor(2 * 1000) await browser.eval(`(function() { window.calledPrefetch = false window.next.router.prefetch = function() { diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 928add00f53a687..036070238794e92 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -150,9 +150,6 @@ const navigateTest = (dev = false) => { let text = await browser.elementByCss('p').text() expect(text).toMatch(/hello.*?world/) - // hydration - await waitFor(2500) - // go to /another async function goFromHomeToAnother() { await browser.eval('window.beforeAnother = true') @@ -322,7 +319,6 @@ const runTests = (dev = false) => { it('should parse query values on mount correctly', async () => { const browser = await webdriver(appPort, '/blog/post-1?another=value') - await waitFor(2000) const text = await browser.elementByCss('#query').text() expect(text).toMatch(/another.*?value/) expect(text).toMatch(/post.*?post-1/) @@ -330,7 +326,6 @@ const runTests = (dev = false) => { it('should reload page on failed data request', async () => { const browser = await webdriver(appPort, '/') - await waitFor(500) await browser.eval('window.beforeClick = true') await browser.elementByCss('#broken-post').click() await waitFor(1000) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 56f48a3676f7d2c..1533089d0a4f829 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -608,7 +608,6 @@ describe('Production Usage', () => { it('should handle AMP correctly in IE', async () => { const browser = await webdriver(appPort, '/some-amp') - await waitFor(1000) const text = await browser.elementByCss('p').text() expect(text).toBe('Not AMP') }) diff --git a/test/integration/production/test/security.js b/test/integration/production/test/security.js index a76efbc36aea923..93eac4bda209519 100644 --- a/test/integration/production/test/security.js +++ b/test/integration/production/test/security.js @@ -6,10 +6,10 @@ import { renderViaHTTP, getBrowserBodyText, waitFor } from 'next-test-utils' import { recursiveReadDir } from 'next/dist/lib/recursive-readdir' import { homedir } from 'os' -// Does the same evaluation checking for INJECTED for 15 seconds, triggering every 500ms +// Does the same evaluation checking for INJECTED for 5 seconds after hydration, triggering every 500ms async function checkInjected(browser) { const start = Date.now() - while (Date.now() - start < 15000) { + while (Date.now() - start < 5000) { const bodyText = await getBrowserBodyText(browser) if (/INJECTED/.test(bodyText)) { throw new Error('Vulnerable to XSS attacks') diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js index 10ae42c581ad4ff..70fd0d332d00dd2 100644 --- a/test/integration/size-limit/test/index.test.js +++ b/test/integration/size-limit/test/index.test.js @@ -81,7 +81,7 @@ describe('Production response size', () => { ) // These numbers are without gzip compression! - const delta = responseSizeKilobytes - 234 + const delta = responseSizeKilobytes - 235 expect(delta).toBeLessThanOrEqual(0) // don't increase size expect(delta).toBeGreaterThanOrEqual(-1) // don't decrease size without updating target }) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index a4bfcb75f6d71a0..0a66a45232c9aef 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -82,7 +82,12 @@ export function runNextCommand(argv, options = {}) { const nextBin = path.join(nextDir, 'dist/bin/next') const cwd = options.cwd || nextDir // Let Next.js decide the environment - const env = { ...process.env, ...options.env, NODE_ENV: '' } + const env = { + ...process.env, + ...options.env, + NODE_ENV: '', + __NEXT_TEST_MODE: 'true', + } return new Promise((resolve, reject) => { console.log(`Running command "next ${argv.join(' ')}"`) diff --git a/test/lib/next-webdriver.d.ts b/test/lib/next-webdriver.d.ts new file mode 100644 index 000000000000000..9c243dcc6f69237 --- /dev/null +++ b/test/lib/next-webdriver.d.ts @@ -0,0 +1,28 @@ +interface Chain { + elementByCss: () => Chain + elementById: () => Chain + getValue: () => Chain + text: () => Chain + type: () => Chain + moveTo: () => Chain + getComputedCss: () => Chain + getAttribute: () => Chain + hasElementByCssSelector: () => Chain + click: () => Chain + elementsByCss: () => Chain + waitForElementByCss: () => Chain + eval: () => Chain + log: () => Chain + url: () => Chain + back: () => Chain + forward: () => Chain + refresh: () => Chain + close: () => Chain + quit: () => Chain +} + +export default function( + appPort: number, + path: string, + waitHydration?: boolean +): Promise diff --git a/test/lib/next-webdriver.js b/test/lib/next-webdriver.js index faf241c16992e2b..bd6d975a50ef032 100644 --- a/test/lib/next-webdriver.js +++ b/test/lib/next-webdriver.js @@ -1,7 +1,9 @@ +/// import os from 'os' import path from 'path' import fetch from 'node-fetch' -import { until, Builder, By } from 'selenium-webdriver' +import Chain from './wd-chain' +import { Builder, By } from 'selenium-webdriver' import { Options as ChromeOptions } from 'selenium-webdriver/chrome' import { Options as SafariOptions } from 'selenium-webdriver/safari' import { Options as FireFoxOptions } from 'selenium-webdriver/firefox' @@ -153,7 +155,7 @@ const freshWindow = async () => { await browser.switchTo().window(newWindow) } -export default async (appPort, path) => { +export default async (appPort, path, waitHydration = true) => { if (!initialWindow) { initialWindow = await browser.getWindowHandle() } @@ -173,137 +175,33 @@ export default async (appPort, path) => { await browser.get(url) console.log(`\n> Loaded browser with ${url}\n`) - class Chain { - updateChain(nextCall) { - if (!this.promise) { - this.promise = Promise.resolve() - } - this.promise = this.promise.then(nextCall) - this.then = cb => this.promise.then(cb) - this.catch = cb => this.promise.catch(cb) - this.finally = cb => this.promise.finally(cb) - return this - } - - elementByCss(sel) { - return this.updateChain(() => - browser.findElement(By.css(sel)).then(el => { - el.sel = sel - el.text = () => el.getText() - el.getComputedCss = prop => el.getCssValue(prop) - el.type = text => el.sendKeys(text) - el.getValue = () => - browser.executeScript( - `return document.querySelector('${sel}').value` - ) - return el - }) - ) - } - - elementById(sel) { - return this.elementByCss(`#${sel}`) - } + // Wait for application to hydrate + if (waitHydration) { + console.log(`\n> Waiting hydration for ${url}\n`) + await browser.executeAsyncScript(function() { + var callback = arguments[arguments.length - 1] - getValue() { - return this.updateChain(el => - browser.executeScript( - `return document.querySelector('${el.sel}').value` - ) - ) - } - - text() { - return this.updateChain(el => el.getText()) - } - - type(text) { - return this.updateChain(el => el.sendKeys(text)) - } - - moveTo() { - return this.updateChain(el => { - return browser - .actions() - .move({ origin: el }) - .perform() - .then(() => el) - }) - } - - getComputedCss(prop) { - return this.updateChain(el => { - return el.getCssValue(prop) - }) - } - - getAttribute(attr) { - return this.updateChain(el => el.getAttribute(attr)) - } - - hasElementByCssSelector(sel) { - return this.eval(`document.querySelector('${sel}')`) - } - - click() { - return this.updateChain(el => { - return el.click().then(() => el) - }) - } - - elementsByCss(sel) { - return this.updateChain(() => browser.findElements(By.css(sel))) - } - - waitForElementByCss(sel, timeout) { - return this.updateChain(() => - browser.wait(until.elementLocated(By.css(sel), timeout)) - ) - } - - eval(snippet) { - if (typeof snippet === 'string' && !snippet.startsWith('return')) { - snippet = `return ${snippet}` + // if it's not a Next.js app return + if (document.documentElement.innerHTML.indexOf('__NEXT_DATA__') === -1) { + callback() } - return this.updateChain(() => browser.executeScript(snippet)) - } - - log(type) { - return this.updateChain(() => - browser - .manage() - .logs() - .get(type) - ) - } - - url() { - return this.updateChain(() => browser.getCurrentUrl()) - } - back() { - return this.updateChain(() => browser.navigate().back()) - } - - forward() { - return this.updateChain(() => browser.navigate().forward()) - } - - refresh() { - return this.updateChain(() => browser.navigate().refresh()) - } - - close() { - return this.updateChain(() => Promise.resolve()) - } - quit() { - return this.close() - } + if (window.__NEXT_HYDRATED) { + callback() + } else { + var timeout = setTimeout(callback, 10 * 1000) + window.__NEXT_HYDRATED_CB = function() { + clearTimeout(timeout) + callback() + } + } + }) + console.log(`\n> Hydration complete for ${url}\n`) } const promiseProp = new Set(['then', 'catch', 'finally']) - return new Proxy(new Chain(), { + return new Proxy(new Chain(browser), { get(obj, prop) { if (obj[prop] || promiseProp.has(prop)) { return obj[prop] diff --git a/test/lib/wd-chain.js b/test/lib/wd-chain.js new file mode 100644 index 000000000000000..0d402eee575f1a8 --- /dev/null +++ b/test/lib/wd-chain.js @@ -0,0 +1,133 @@ +import { until, By } from 'selenium-webdriver' + +export default class Chain { + constructor(browser) { + this.browser = browser + } + + updateChain(nextCall) { + if (!this.promise) { + this.promise = Promise.resolve() + } + this.promise = this.promise.then(nextCall) + this.then = cb => this.promise.then(cb) + this.catch = cb => this.promise.catch(cb) + this.finally = cb => this.promise.finally(cb) + return this + } + + elementByCss(sel) { + return this.updateChain(() => + this.browser.findElement(By.css(sel)).then(el => { + el.sel = sel + el.text = () => el.getText() + el.getComputedCss = prop => el.getCssValue(prop) + el.type = text => el.sendKeys(text) + el.getValue = () => + this.browser.executeScript( + `return document.querySelector('${sel}').value` + ) + return el + }) + ) + } + + elementById(sel) { + return this.elementByCss(`#${sel}`) + } + + getValue() { + return this.updateChain(el => + this.browser.executeScript( + `return document.querySelector('${el.sel}').value` + ) + ) + } + + text() { + return this.updateChain(el => el.getText()) + } + + type(text) { + return this.updateChain(el => el.sendKeys(text)) + } + + moveTo() { + return this.updateChain(el => { + return this.browser + .actions() + .move({ origin: el }) + .perform() + .then(() => el) + }) + } + + getComputedCss(prop) { + return this.updateChain(el => { + return el.getCssValue(prop) + }) + } + + getAttribute(attr) { + return this.updateChain(el => el.getAttribute(attr)) + } + + hasElementByCssSelector(sel) { + return this.eval(`document.querySelector('${sel}')`) + } + + click() { + return this.updateChain(el => { + return el.click().then(() => el) + }) + } + + elementsByCss(sel) { + return this.updateChain(() => this.browser.findElements(By.css(sel))) + } + + waitForElementByCss(sel, timeout) { + return this.updateChain(() => + this.browser.wait(until.elementLocated(By.css(sel), timeout)) + ) + } + + eval(snippet) { + if (typeof snippet === 'string' && !snippet.startsWith('return')) { + snippet = `return ${snippet}` + } + return this.updateChain(() => this.browser.executeScript(snippet)) + } + + log(type) { + return this.updateChain(() => + this.browser + .manage() + .logs() + .get(type) + ) + } + + url() { + return this.updateChain(() => this.browser.getCurrentUrl()) + } + + back() { + return this.updateChain(() => this.browser.navigate().back()) + } + + forward() { + return this.updateChain(() => this.browser.navigate().forward()) + } + + refresh() { + return this.updateChain(() => this.browser.navigate().refresh()) + } + + close() { + return this.updateChain(() => Promise.resolve()) + } + quit() { + return this.close() + } +} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000000000..010d7d03ce9685e --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "es6", + "allowJs": true, + "baseUrl": "./lib" + }, + "include": ["./**/*"] +}