Merge pull request #673 from mrkishi/testing-issue

Harden tests (...a bit)
This commit is contained in:
Rich Harris
2019-05-09 08:57:18 -04:00
committed by GitHub
35 changed files with 726 additions and 685 deletions

4
.gitignore vendored
View File

@@ -3,11 +3,7 @@ yarn.lock
yarn-error.log yarn-error.log
node_modules node_modules
cypress/screenshots cypress/screenshots
test/app/.sapper
test/app/src/manifest
__sapper__ __sapper__
test/app/export
test/app/build
sapper sapper
runtime.js runtime.js
dist dist

View File

@@ -1,49 +1,77 @@
import * as path from 'path'; import * as path from 'path';
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
import * as ports from 'port-authority';
import { fork, ChildProcess } from 'child_process'; import { fork, ChildProcess } from 'child_process';
import { AddressInfo } from 'net';
import { wait } from '../utils'
const DEFAULT_ENTRY = '__sapper__/build/server/server.js';
const DELAY = parseInt(process.env.SAPPER_TEST_DELAY) || 50;
declare const start: () => Promise<void>; declare const start: () => Promise<void>;
declare const prefetchRoutes: () => Promise<void>; declare const prefetchRoutes: () => Promise<void>;
declare const prefetch: (href: string) => Promise<void>; declare const prefetch: (href: string) => Promise<void>;
declare const goto: (href: string) => Promise<void>; declare const goto: (href: string) => Promise<void>;
type StartOpts = {
requestInterceptor?: (interceptedRequst: puppeteer.Request) => any
};
export class AppRunner { export class AppRunner {
cwd: string; exiting: boolean;
entry: string; terminate: Promise<any>;
port: number;
proc: ChildProcess; server: ChildProcess;
address: AddressInfo;
base: string;
messages: any[]; messages: any[];
errors: Error[];
browser: puppeteer.Browser; browser: puppeteer.Browser;
page: puppeteer.Page; page: puppeteer.Page;
constructor(cwd: string, entry: string) { sapper = {
this.cwd = cwd; start: () => this.page.evaluate(() => start()).then(() => void 0),
this.entry = path.join(cwd, entry); prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0)
};
constructor() {
this.messages = []; this.messages = [];
this.errors = [];
} }
async start({ requestInterceptor }: StartOpts = {}) { async start(cwd: string, entry: string = DEFAULT_ENTRY) {
this.port = await ports.find(3000); const server_listening = deferred();
const server_closed = deferred();
const browser_closed = deferred();
this.proc = fork(this.entry, [], { this.terminate = Promise.all([server_closed, browser_closed]);
cwd: this.cwd,
env: { this.server = fork(path.join(cwd, entry), [], { cwd });
PORT: String(this.port) this.server.on('exit', () => {
server_listening.reject();
server_closed.settle(this.exiting);
});
this.server.on('message', message => {
if (!message.__sapper__) return;
switch (message.event) {
case 'listening':
this.address = message.address;
this.base = `http://localhost:${this.address.port}`;
server_listening.resolve();
break;
case 'error':
this.errors.push(Object.assign(new Error(), message.error));
break;
default:
this.messages.push(message);
} }
}); });
this.proc.on('message', message => {
if (!message.__sapper__) return;
this.messages.push(message);
});
this.browser = await puppeteer.launch({ args: ['--no-sandbox'] }); this.browser = await puppeteer.launch({ args: ['--no-sandbox'] });
this.browser.on('disconnected', () => browser_closed.settle(this.exiting));
this.page = await this.browser.newPage(); this.page = await this.browser.newPage();
this.page.on('console', msg => { this.page.on('console', msg => {
@@ -54,25 +82,28 @@ export class AppRunner {
} }
}); });
if (requestInterceptor) { await server_listening;
await this.page.setRequestInterception(true);
this.page.on('request', requestInterceptor);
}
return { return this;
page: this.page,
base: `http://localhost:${this.port}`,
// helpers
start: () => this.page.evaluate(() => start()).then(() => void 0),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0),
title: () => this.page.$eval('h1', node => node.textContent).then(serializable => String(serializable))
};
} }
capture(fn: () => any): Promise<string[]> { load(url: string) {
if (url[0] === '/') {
url = `${this.base}${url}`;
}
return this.page.goto(url);
}
text(selector: string) {
return this.page.$eval(selector, node => node.textContent);
}
wait(extra_ms: number = 0) {
return wait(DELAY + extra_ms);
}
capture_requests(fn: () => any): Promise<string[]> {
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
const requests: string[] = []; const requests: string[] = [];
const pending: Set<string> = new Set(); const pending: Set<string> = new Set();
@@ -120,13 +151,55 @@ export class AppRunner {
}); });
} }
end() { async intercept_requests(interceptor: (request: puppeteer.Request) => void, fn: () => any): Promise<void> {
return Promise.all([ const unique_interceptor = request => interceptor(request);
this.browser.close(),
new Promise(fulfil => { this.page.prependListener('request', unique_interceptor);
this.proc.once('exit', fulfil); await this.page.setRequestInterception(true);
this.proc.kill();
}) const result = await Promise.resolve(fn());
]);
await this.page.setRequestInterception(false);
this.page.removeListener('request', unique_interceptor);
return result;
} }
}
end() {
this.exiting = true;
this.server.kill();
this.browser.close();
return this.terminate;
}
}
interface Deferred<T> extends Promise<T> {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
settle: (result?: boolean) => void;
}
function settle<T>(this: Deferred<T>, result: boolean) {
if (result) {
this.resolve();
} else {
this.reject();
}
}
function deferred<T>() {
let resolve, reject;
const deferred = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
}) as Deferred<T>;
deferred.resolve = resolve;
deferred.reject = reject;
deferred.settle = settle;
return deferred;
}

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware())
.listen(PORT);
start(app);

View File

@@ -1,23 +1,23 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as http from 'http'; import * as http from 'http';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
declare let deleted: { id: number }; declare let deleted: { id: number };
declare let el: any; declare let el: any;
function get(url: string, opts?: any): Promise<{ headers: Record<string, string>, body: string }> { type Response = { headers: http.IncomingHttpHeaders, body: string };
function get(url: string, opts: http.RequestOptions = {}): Promise<Response> {
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
const req = http.get(url, opts || {}, res => { const req = http.get(url, opts, res => {
res.on('error', reject); res.on('error', reject);
let body = ''; let body = '';
res.on('data', chunk => body += chunk); res.on('data', chunk => body += chunk);
res.on('end', () => { res.on('end', () => {
fulfil({ fulfil({
headers: res.headers as Record<string, string>, headers: res.headers,
body body
}); });
}); });
@@ -30,114 +30,104 @@ function get(url: string, opts?: any): Promise<{ headers: Record<string, string>
describe('basics', function() { describe('basics', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('serves /', async () => { it('serves /', async () => {
await page.goto(base); await r.load('/');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'Great success!' 'Great success!'
); );
}); });
it('serves /?', async () => { it('serves /?', async () => {
await page.goto(`${base}?`); await r.load('/?');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'Great success!' 'Great success!'
); );
}); });
it('serves static route', async () => { it('serves static route', async () => {
await page.goto(`${base}/a`); await r.load('/a');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'a' 'a'
); );
}); });
it('serves static route from dir/index.html file', async () => { it('serves static route from dir/index.html file', async () => {
await page.goto(`${base}/b`); await r.load('/b');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'b' 'b'
); );
}); });
it('serves dynamic route', async () => { it('serves dynamic route', async () => {
await page.goto(`${base}/test-slug`); await r.load('/test-slug');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'TEST-SLUG' 'TEST-SLUG'
); );
}); });
it('navigates to a new page without reloading', async () => { it('navigates to a new page without reloading', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
const requests: string[] = await runner.capture(async () => { const requests: string[] = await r.capture_requests(async () => {
await page.click('a[href="a"]'); await r.page.click('a[href="a"]');
await r.wait();
}); });
assert.deepEqual(requests, []); assert.deepEqual(requests, []);
assert.equal( assert.equal(
await title(), await r.text('h1'),
'a' 'a'
); );
}); });
it('navigates programmatically', async () => { it('navigates programmatically', async () => {
await page.goto(`${base}/a`); await r.load('/a');
await start(); await r.sapper.start();
await r.sapper.goto('b');
await goto('b');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'b' 'b'
); );
}); });
it('prefetches programmatically', async () => { it('prefetches programmatically', async () => {
await page.goto(`${base}/a`); await r.load(`/a`);
await start(); await r.sapper.start();
const requests = await runner.capture(() => prefetch('b')); const requests = await r.capture_requests(() => r.sapper.prefetch('b'));
assert.equal(requests.length, 2); assert.equal(requests.length, 2);
assert.equal(requests[1], `${base}/b.json`); assert.equal(requests[1], `${r.base}/b.json`);
}); });
// TODO equivalent test for a webpack app // TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => { it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
const { headers } = await get(base); const { headers } = await get(r.base);
assert.equal( assert.equal(
headers['content-type'], headers['content-type'],
@@ -157,162 +147,163 @@ describe('basics', function() {
}); });
it('calls a delete handler', async () => { it('calls a delete handler', async () => {
await page.goto(`${base}/delete-test`); await r.load('/delete-test');
await start(); await r.sapper.start();
await page.click('.del'); await r.page.click('.del');
await page.waitForFunction(() => deleted); await r.page.waitForFunction(() => deleted);
assert.equal(await page.evaluate(() => deleted.id), 42); assert.equal(await r.page.evaluate(() => deleted.id), 42);
}); });
it('hydrates initial route', async () => { it('hydrates initial route', async () => {
await page.goto(base); await r.load('/');
await page.evaluate(() => { await r.page.evaluate(() => {
el = document.querySelector('.hydrate-test'); el = document.querySelector('.hydrate-test');
}); });
await start(); await r.sapper.start();
assert.ok(await page.evaluate(() => { assert.ok(await r.page.evaluate(() => {
return document.querySelector('.hydrate-test') === el; return document.querySelector('.hydrate-test') === el;
})); }));
}); });
it('does not attempt client-side navigation to server routes', async () => { it('does not attempt client-side navigation to server routes', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click(`[href="ambiguous/ok.json"]`); await r.page.click('[href="ambiguous/ok.json"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => document.body.textContent), await r.text('body'),
'ok' 'ok'
); );
}); });
it('allows reserved words as route names', async () => { it('allows reserved words as route names', async () => {
await page.goto(`${base}/const`); await r.load('/const');
await start(); await r.sapper.start();
assert.equal( assert.equal(
await title(), await r.text('h1'),
'reserved words are okay as routes' 'reserved words are okay as routes'
); );
}); });
it('accepts value-less query string parameter on server', async () => { it('accepts value-less query string parameter on server', async () => {
await page.goto(`${base}/echo-query?message`); await r.load('/echo-query?message');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'{"message":""}' '{"message":""}'
); );
}); });
it('accepts value-less query string parameter on client', async () => { it('accepts value-less query string parameter on client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a[href="echo-query?message"]') await r.page.click('a[href="echo-query?message"]');
await r.wait();
assert.equal( assert.equal(
await title(), await r.text('h1'),
'{"message":""}' '{"message":""}'
); );
}); });
it('accepts duplicated query string parameter on server', async () => { it('accepts duplicated query string parameter on server', async () => {
await page.goto(`${base}/echo-query?p=one&p=two`); await r.load('/echo-query?p=one&p=two');
assert.equal( assert.equal(
await title(), await r.text('h1'),
'{"p":["one","two"]}' '{"p":["one","two"]}'
); );
}); });
it('accepts duplicated query string parameter on client', async () => { it('accepts duplicated query string parameter on client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a[href="echo-query?p=one&p=two"]') await r.page.click('a[href="echo-query?p=one&p=two"]')
assert.equal( assert.equal(
await title(), await r.text('h1'),
'{"p":["one","two"]}' '{"p":["one","two"]}'
); );
}); });
// skipped because Nightmare doesn't seem to focus the <a> correctly // skipped because Nightmare doesn't seem to focus the <a> correctly
it('resets the active element after navigation', async () => { it('resets the active element after navigation', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="a"]'); await r.page.click('[href="a"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => document.activeElement.nodeName), await r.page.evaluate(() => document.activeElement.nodeName),
'BODY' 'BODY'
); );
}); });
it('replaces %sapper.xxx% tags safely', async () => { it('replaces %sapper.xxx% tags safely', async () => {
await page.goto(`${base}/unsafe-replacement`); await r.load('/unsafe-replacement');
await start(); await r.sapper.start();
const html = String(await page.evaluate(() => document.body.innerHTML)); const html = String(await r.page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1); assert.equal(html.indexOf('%sapper'), -1);
}); });
it('navigates between routes with empty parts', async () => { it('navigates between routes with empty parts', async () => {
await page.goto(`${base}/dirs/foo`); await r.load('/dirs/foo');
await start(); await r.sapper.start();
assert.equal(await title(), 'foo'); assert.equal(await r.text('h1'), 'foo');
await page.click('[href="dirs/bar"]'); await r.page.click('[href="dirs/bar"]');
await wait(50); await r.wait();
assert.equal(await title(), 'bar'); assert.equal(await r.text('h1'), 'bar');
}); });
it('navigates to ...rest', async () => { it('navigates to ...rest', async () => {
await page.goto(`${base}/abc/xyz`); await r.load('/abc/xyz');
await start(); await r.sapper.start();
assert.equal(await title(), 'abc,xyz'); assert.equal(await r.text('h1'), 'abc,xyz');
await page.click('[href="xyz/abc/deep"]'); await r.page.click('[href="xyz/abc/deep"]');
await wait(50); await r.wait();
assert.equal(await title(), 'xyz,abc'); assert.equal(await r.text('h1'), 'xyz,abc');
await page.click(`[href="xyz/abc/qwe/deep.json"]`); await r.page.click('[href="xyz/abc/qwe/deep.json"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => document.body.textContent), await r.text('body'),
'xyz,abc,qwe' 'xyz,abc,qwe'
); );
}); });
it('navigates between dynamic routes with same segments', async () => { it('navigates between dynamic routes with same segments', async () => {
await page.goto(`${base}/dirs/bar/xyz`); await r.load('/dirs/bar/xyz');
await start(); await r.sapper.start();
assert.equal(await title(), 'A page'); assert.equal(await r.text('h1'), 'A page');
await page.click('[href="dirs/foo/xyz"]'); await r.page.click('[href="dirs/foo/xyz"]');
await wait(50); await r.wait();
assert.equal(await title(), 'B page'); assert.equal(await r.text('h1'), 'B page');
}); });
it('runs server route handlers before page handlers, if they match', async () => { it('runs server route handlers before page handlers, if they match', async () => {
const json = await get(`${base}/middleware`, { const json = await get(`${r.base}/middleware`, {
headers: { headers: {
'Accept': 'application/json' 'Accept': 'application/json'
} }
@@ -320,22 +311,26 @@ describe('basics', function() {
assert.equal(json.body, '{"json":true}'); assert.equal(json.body, '{"json":true}');
const html = await get(`${base}/middleware`); const html = await get(`${r.base}/middleware`);
assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1); assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1);
}); });
it('invalidates page when a segment is skipped', async () => { it('invalidates page when a segment is skipped', async () => {
await page.goto(`${base}/skipped/x/1`); await r.load('/skipped/x/1');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a[href="skipped/y/1"]'); await r.page.click('a[href="skipped/y/1"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await title(), await r.text('h1'),
'y:1' 'y:1'
); );
}); });
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
}); });

42
test/apps/common.js Normal file
View File

@@ -0,0 +1,42 @@
const { NODE_ENV, PORT } = process.env;
export const dev = NODE_ENV === 'development';
export function start(app) {
const port = parseInt(PORT) || 0;
app.listen(port, () => {
const address = app.server.address();
process.env.PORT = address.port;
send({
__sapper__: true,
event: 'listening',
address
});
});
}
const properties = ['name', 'message', 'stack', 'code', 'lineNumber', 'fileName'];
function send(message) {
process.send && process.send(message);
}
function send_error(error) {
send({
__sapper__: true,
event: 'error',
error: properties.reduce((object, key) => ({...object, [key]: error[key]}), {})
})
}
process.on('unhandledRejection', (reason, p) => {
send_error(reason);
});
process.on('uncaughtException', err => {
send_error(err);
process.exitCode = 1;
});

View File

@@ -1,13 +1,14 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use((req, res, next) => { .use((req, res, next) => {
// set test cookie // set test cookie
res.setHeader('Set-Cookie', ['a=1; Max-Age=3600', 'b=2; Max-Age=3600']); res.setHeader('Set-Cookie', ['a=1; Max-Age=3600', 'b=2; Max-Age=3600']);
next(); next();
}) })
.use(sapper.middleware()) .use(sapper.middleware())
.listen(PORT);
start(app);

View File

@@ -1,59 +1,54 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { wait } from '../../utils';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
describe('credentials', function() { describe('credentials', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('sends cookies when using this.fetch with credentials: "include"', async () => { it('sends cookies when using this.fetch with credentials: "include"', async () => {
await page.goto(`${base}/credentials?creds=include`); await r.load('/credentials?creds=include');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'a: 1, b: 2, max-age: undefined' 'a: 1, b: 2, max-age: undefined'
); );
}); });
it('does not send cookies when using this.fetch without credentials', async () => { it('does not send cookies when using this.fetch without credentials', async () => {
await page.goto(`${base}/credentials`); await r.load('/credentials');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'unauthorized' 'unauthorized'
); );
}); });
it('delegates to fetch on the client', async () => { it('delegates to fetch on the client', async () => {
await page.goto(base) await r.load('/')
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="credentials?creds=include"]'); await r.page.click('[href="credentials?creds=include"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'a: 1, b: 2, max-age: undefined' 'a: 1, b: 2, max-age: undefined'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,78 +1,61 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('css', function() { describe('css', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('includes critical CSS with server render', async () => { it('includes critical CSS with server render', async () => {
await page.goto(base); await r.load('/');
assert.equal( assert.equal(
await page.evaluate(() => { await r.page.$eval('h1', node => getComputedStyle(node).color),
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(255, 0, 0)' 'rgb(255, 0, 0)'
); );
}); });
it('loads CSS when navigating client-side', async () => { it('loads CSS when navigating client-side', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click(`[href="foo"]`); await r.page.click(`[href="foo"]`);
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => { await r.page.$eval('h1', node => getComputedStyle(node).color),
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(0, 0, 255)' 'rgb(0, 0, 255)'
); );
}); });
it('loads CSS for a lazily-rendered component', async () => { it('loads CSS for a lazily-rendered component', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click(`[href="bar"]`); await r.page.click(`[href="bar"]`);
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => { await r.page.$eval('h1', node => getComputedStyle(node).color),
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(0, 128, 0)' 'rgb(0, 128, 0)'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,68 +1,63 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('encoding', function() { describe('encoding', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('encodes routes', async () => { it('encodes routes', async () => {
await page.goto(`${base}/fünke`); await r.load('/fünke');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
`I'm afraid I just blue myself` `I'm afraid I just blue myself`
); );
}); });
it('encodes req.params and req.query for server-rendered pages', async () => { it('encodes req.params and req.query for server-rendered pages', async () => {
await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789`); await r.load('/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
); );
}); });
it('encodes req.params and req.query for client-rendered pages', async () => { it('encodes req.params and req.query for client-rendered pages', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a'); await r.page.click('a');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
); );
}); });
it('encodes req.params for server routes', async () => { it('encodes req.params for server routes', async () => {
await page.goto(`${base}/echo/server-route/encöded`); await r.load('/echo/server-route/encöded');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'encöded' 'encöded'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,5 +1,4 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils'; import { wait } from '../../utils';
@@ -7,142 +6,137 @@ import { wait } from '../../utils';
describe('errors', function() { describe('errors', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('handles missing route on server', async () => { it('handles missing route on server', async () => {
await page.goto(`${base}/nope`); await r.load('/nope');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'404' '404'
); );
}); });
it('handles missing route on client', async () => { it('handles missing route on client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await page.click('[href="nope"]'); await r.page.click('[href="nope"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'404' '404'
); );
}); });
it('handles explicit 4xx on server', async () => { it('handles explicit 4xx on server', async () => {
await page.goto(`${base}/blog/nope`); await r.load('/blog/nope');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'404' '404'
); );
}); });
it('handles explicit 4xx on client', async () => { it('handles explicit 4xx on client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="blog/nope"]'); await r.page.click('[href="blog/nope"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'404' '404'
); );
}); });
it('handles error on server', async () => { it('handles error on server', async () => {
await page.goto(`${base}/throw`); await r.load('/throw');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'500' '500'
); );
}); });
it('handles error on client', async () => { it('handles error on client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="throw"]'); await r.page.click('[href="throw"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'500' '500'
); );
}); });
it('does not serve error page for explicit non-page errors', async () => { it('does not serve error page for explicit non-page errors', async () => {
await page.goto(`${base}/nope.json`); await r.load('/nope.json');
assert.equal( assert.equal(
await page.evaluate(() => document.body.textContent), await r.text('body'),
'nope' 'nope'
); );
}); });
it('does not serve error page for thrown non-page errors', async () => { it('does not serve error page for thrown non-page errors', async () => {
await page.goto(`${base}/throw.json`); await r.load('/throw.json');
assert.equal( assert.equal(
await page.evaluate(() => document.body.textContent), await r.text('body'),
'oops' 'oops'
); );
}); });
it('execute error page hooks', async () => { it('execute error page hooks', async () => {
await page.goto(`${base}/some-throw-page`); await r.load('/some-throw-page');
await start(); await r.sapper.start();
await wait(50);
assert.equal( assert.equal(
await page.$eval('h2', node => node.textContent), await r.text('h2'),
'success' 'success'
); );
}) })
it('does not serve error page for async non-page error', async () => { it('does not serve error page for async non-page error', async () => {
await page.goto(`${base}/async-throw.json`); await r.load('/async-throw.json');
assert.equal( assert.equal(
await page.evaluate(() => document.body.textContent), await r.text('body'),
'oops' 'oops'
); );
}); });
it('clears props.error on successful render', async () => { it('clears props.error on successful render', async () => {
await page.goto(`${base}/no-error`); await r.load('/no-error');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="enhance-your-calm"]'); await r.page.click('[href="enhance-your-calm"]');
await wait(50); await r.wait();
assert.equal(await title(), '420'); assert.equal(await r.text('h1'), '420');
await page.goBack(); await r.page.goBack();
await wait(50); await r.wait();
assert.equal(await title(), 'No error here'); assert.equal(await r.text('h1'), 'No error here');
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
}); });
}); });

View File

@@ -2,14 +2,12 @@ import sirv from 'sirv';
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env; import { start, dev } from '../../common.js';
const dev = NODE_ENV === 'development';
polka() const app = polka()
.use( .use(
sirv('static', { dev }), sirv('static', { dev }),
sapper.middleware() sapper.middleware()
) );
.listen(PORT, err => {
if (err) console.log('error', err); start(app);
});

View File

@@ -6,14 +6,12 @@ describe('export-webpack', function() {
this.timeout(10000); this.timeout(10000);
// hooks // hooks
before(async () => { before('build app', () => api.build({ cwd: __dirname, bundler: 'webpack' }));
await api.build({ cwd: __dirname, bundler: 'webpack' }); before('export app', () => api.export({ cwd: __dirname }));
await api.export({ cwd: __dirname, bundler: 'webpack' });
});
// tests
it('injects <link rel=preload> tags', () => { it('injects <link rel=preload> tags', () => {
const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8'); const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8');
assert.ok(/rel=preload/.test(index)); assert.ok(/rel=preload/.test(index));
}); });
}); });

View File

@@ -2,14 +2,12 @@ import sirv from 'sirv';
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env; import { start, dev } from '../../common.js';
const dev = NODE_ENV === 'development';
polka() const app = polka()
.use( .use(
sirv('static', { dev }), sirv('static', { dev }),
sapper.middleware() sapper.middleware()
) );
.listen(PORT, err => {
if (err) console.log('error', err); start(app);
});

View File

@@ -6,11 +6,10 @@ describe('export', function() {
this.timeout(10000); this.timeout(10000);
// hooks // hooks
before(async () => { before('build app', () => api.build({ cwd: __dirname }));
await api.build({ cwd: __dirname }); before('export app', () => api.export({ cwd: __dirname }));
await api.export({ cwd: __dirname });
});
// tests
it('crawls a site', () => { it('crawls a site', () => {
const files = walk(`${__dirname}/__sapper__/export`); const files = walk(`${__dirname}/__sapper__/export`);
@@ -45,4 +44,4 @@ describe('export', function() {
}); });
// TODO test timeout, basepath // TODO test timeout, basepath
}); });

View File

@@ -1,7 +1,7 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
const app = polka().use(sapper.middleware({ const app = polka().use(sapper.middleware({
ignore: [ ignore: [
@@ -16,4 +16,4 @@ const app = polka().use(sapper.middleware({
app.get('/'+uri, (req, res) => res.end(uri)); app.get('/'+uri, (req, res) => res.end(uri));
}); });
app.listen(PORT); start(app);

View File

@@ -1,58 +1,58 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
describe('ignore', function() { describe('ignore', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('respects `options.ignore` values (RegExp)', async () => { it('respects `options.ignore` values (RegExp)', async () => {
await page.goto(`${base}/foobar`); await r.load('/foobar');
assert.equal( assert.equal(
await page.evaluate(() => document.documentElement.textContent), await r.text('body'),
'foobar' 'foobar'
); );
}); });
it('respects `options.ignore` values (String #1)', async () => { it('respects `options.ignore` values (String #1)', async () => {
await page.goto(`${base}/buzz`); await r.load('/buzz');
assert.equal( assert.equal(
await page.evaluate(() => document.documentElement.textContent), await r.text('body'),
'buzz' 'buzz'
); );
}); });
it('respects `options.ignore` values (String #2)', async () => { it('respects `options.ignore` values (String #2)', async () => {
await page.goto(`${base}/fizzer`); await r.load('/fizzer');
assert.equal( assert.equal(
await page.evaluate(() => document.documentElement.textContent), await r.text('body'),
'fizzer' 'fizzer'
); );
}); });
it('respects `options.ignore` values (Function)', async () => { it('respects `options.ignore` values (Function)', async () => {
await page.goto(`${base}/hello`); await r.load('/hello');
assert.equal( assert.equal(
await page.evaluate(() => document.documentElement.textContent), await r.text('body'),
'hello' 'hello'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,33 +1,25 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('layout', function() { describe('layout', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('only recreates components when necessary', async () => { it('only recreates components when necessary', async () => {
await page.goto(`${base}/foo/bar/baz`); await r.load('/foo/bar/baz');
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); const text1 = await r.text('#sapper');
assert.deepEqual(text1.split('\n').map(str => str.trim()).filter(Boolean), [ assert.deepEqual(text1.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1', 'y: bar 1',
'z: baz 1', 'z: baz 1',
@@ -35,8 +27,8 @@ describe('layout', function() {
'child segment: baz' 'child segment: baz'
]); ]);
await start(); await r.sapper.start();
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); const text2 = await r.text('#sapper');
assert.deepEqual(text2.split('\n').map(str => str.trim()).filter(Boolean), [ assert.deepEqual(text2.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1', 'y: bar 1',
'z: baz 1', 'z: baz 1',
@@ -44,10 +36,10 @@ describe('layout', function() {
'child segment: baz' 'child segment: baz'
]); ]);
await page.click('[href="foo/bar/qux"]'); await r.page.click('[href="foo/bar/qux"]');
await wait(50); await r.wait();
const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); const text3 = await r.text('#sapper');
assert.deepEqual(text3.split('\n').map(str => str.trim()).filter(Boolean), [ assert.deepEqual(text3.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1', 'y: bar 1',
'z: qux 2', 'z: qux 2',
@@ -55,4 +47,8 @@ describe('layout', function() {
'child segment: qux' 'child segment: qux'
]); ]);
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,7 +1,5 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { wait } from '../../utils';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
declare const fulfil: () => Promise<void>; declare const fulfil: () => Promise<void>;
@@ -9,112 +7,106 @@ declare const fulfil: () => Promise<void>;
describe('preloading', function() { describe('preloading', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('serializes Set objects returned from preload', async () => { it('serializes Set objects returned from preload', async () => {
await page.goto(`${base}/preload-values/set`); await r.load('/preload-values/set');
assert.equal(await title(), 'true'); assert.equal(await r.text('h1'), 'true');
await start(); await r.sapper.start();
assert.equal(await title(), 'true'); assert.equal(await r.text('h1'), 'true');
}); });
it('prevent crash if preload return nothing', async () => { it('prevent crash if preload return nothing', async () => {
await page.goto(`${base}/preload-nothing`); await r.load('/preload-nothing');
await start(); await r.sapper.start();
await wait(50);
assert.equal(await title(), 'Page loaded'); assert.equal(await r.text('h1'), 'Page loaded');
}); });
it('bails on custom classes returned from preload', async () => { it('bails on custom classes returned from preload', async () => {
await page.goto(`${base}/preload-values/custom-class`); await r.load('/preload-values/custom-class');
assert.equal(await title(), '42'); assert.equal(await r.text('h1'), '42');
await start(); await r.sapper.start();
assert.equal(await title(), '42'); assert.equal(await r.text('h1'), '42');
}); });
it('sets preloading true when appropriate', async () => { it('sets preloading true when appropriate', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a[href="slow-preload"]'); await r.page.click('a[href="slow-preload"]');
assert.ok(await page.evaluate(() => !!document.querySelector('progress'))); assert.ok(await r.page.evaluate(() => !!document.querySelector('progress')));
await page.evaluate(() => fulfil()); await r.page.evaluate(() => fulfil());
assert.ok(await page.evaluate(() => !document.querySelector('progress'))); assert.ok(await r.page.evaluate(() => !document.querySelector('progress')));
}); });
it('runs preload in root component', async () => { it('runs preload in root component', async () => {
await page.goto(`${base}/preload-root`); await r.load('/preload-root');
assert.equal(await title(), 'root preload function ran: true'); assert.equal(await r.text('h1'), 'root preload function ran: true');
}); });
it('cancels navigation if subsequent navigation occurs during preload', async () => { it('cancels navigation if subsequent navigation occurs during preload', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('a[href="slow-preload"]'); await r.page.click('a[href="slow-preload"]');
await wait(100); await r.wait();
await page.click('a[href="foo"]'); await r.page.click('a[href="foo"]');
assert.equal(page.url(), `${base}/foo`); assert.equal(r.page.url(), `${r.base}/foo`);
assert.equal(await title(), 'foo'); assert.equal(await r.text('h1'), 'foo');
await page.evaluate(() => fulfil()); await r.page.evaluate(() => fulfil());
await wait(100); await r.wait();
assert.equal(page.url(), `${base}/foo`); assert.equal(r.page.url(), `${r.base}/foo`);
assert.equal(await title(), 'foo'); assert.equal(await r.text('h1'), 'foo');
}); });
it('navigates to prefetched urls', async () => { it('navigates to prefetched urls', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.hover('a[href="prefetch/qwe"]'); await r.page.hover('a[href="prefetch/qwe"]');
await wait(100); await r.wait(50);
await page.hover('a[href="prefetch/xyz"]'); await r.page.hover('a[href="prefetch/xyz"]');
await wait(100); await r.wait(50);
await page.click('a[href="prefetch/qwe"]'); await r.page.click('a[href="prefetch/qwe"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await title(), await r.text('h1'),
'qwe' 'qwe'
); );
await page.goto(`${base}/prefetch`); await r.load('/prefetch');
await wait(50);
assert.equal( assert.equal(
await title(), await r.text('h1'),
'prefetch' 'prefetch'
); );
}); });
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
}); });

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -2,138 +2,137 @@ import * as assert from 'assert';
import * as puppeteer from 'puppeteer'; import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('redirects', function() { describe('redirects', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start({
requestInterceptor: (interceptedRequest) => {
if (/example\.com/.test(interceptedRequest.url())) {
interceptedRequest.respond({
status: 200,
contentType: 'text/html',
body: `<h1>external</h1>`
});
} else {
interceptedRequest.continue();
}
}
}));
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('redirects on server', async () => { it('redirects on server', async () => {
await page.goto(`${base}/redirect-from`); await r.load('/redirect-from');
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/redirect-to` `${r.base}/redirect-to`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'redirected' 'redirected'
); );
}); });
it('redirects in client', async () => { it('redirects in client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="redirect-from"]'); await r.page.click('[href="redirect-from"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/redirect-to` `${r.base}/redirect-to`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'redirected' 'redirected'
); );
}); });
it('redirects to root on server', async () => { it('redirects to root on server', async () => {
await page.goto(`${base}/redirect-to-root`); await r.load('/redirect-to-root');
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/` `${r.base}/`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'root' 'root'
); );
}); });
it('redirects to root in client', async () => { it('redirects to root in client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="redirect-to-root"]'); await r.page.click('[href="redirect-to-root"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/` `${r.base}/`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'root' 'root'
); );
}); });
const interceptor = (request: puppeteer.Request) => {
if (/example\.com/.test(request.url())) {
request.respond({
status: 200,
contentType: 'text/html',
body: `<h1>external</h1>`
});
} else {
request.continue();
}
};
it('redirects to external URL on server', async () => { it('redirects to external URL on server', async () => {
await page.goto(`${base}/redirect-to-external`); await r.intercept_requests(interceptor, async () => {
await r.load('/redirect-to-external');
});
assert.equal( assert.equal(
page.url(), r.page.url(),
`https://example.com/` `https://example.com/`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'external' 'external'
); );
}); });
it('redirects to external URL in client', async () => { it('redirects to external URL in client', async () => {
await page.goto(base); await r.load('/');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="redirect-to-external"]'); await r.intercept_requests(interceptor, async () => {
await wait(50); await r.page.click('[href="redirect-to-external"]');
await r.wait();
});
assert.equal( assert.equal(
page.url(), r.page.url(),
`https://example.com/` `https://example.com/`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'external' 'external'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -1,93 +1,87 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('scroll', function() { describe('scroll', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('scrolls to active deeplink', async () => { it('scrolls to active deeplink', async () => {
await page.goto(`${base}/tall-page#foo`); await r.load('/tall-page#foo');
await start(); await r.sapper.start();
const scrollY = await page.evaluate(() => window.scrollY); const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY)); assert.ok(scrollY > 0, String(scrollY));
}); });
it('scrolls to any deeplink if it was already active', async () => { it('scrolls to any deeplink if it was already active', async () => {
await page.goto(`${base}/tall-page#foo`); await r.load('/tall-page#foo');
await start(); await r.sapper.start();
let scrollY = await page.evaluate(() => window.scrollY); let scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY)); assert.ok(scrollY > 0, String(scrollY));
scrollY = await page.evaluate(() => { scrollY = await r.page.evaluate(() => {
window.scrollTo(0, 0) window.scrollTo(0, 0)
return window.scrollY return window.scrollY
}); });
assert.ok(scrollY === 0, String(scrollY)); assert.ok(scrollY === 0, String(scrollY));
await page.click('[href="tall-page#foo"]'); await r.page.click('[href="tall-page#foo"]');
scrollY = await page.evaluate(() => window.scrollY); scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY)); assert.ok(scrollY > 0, String(scrollY));
}); });
it('resets scroll when a link is clicked', async () => { it('resets scroll when a link is clicked', async () => {
await page.goto(`${base}/tall-page#foo`); await r.load('/tall-page#foo');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page"]'); await r.page.click('[href="another-tall-page"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
await page.evaluate(() => window.scrollY), await r.page.evaluate(() => window.scrollY),
0 0
); );
}); });
it('preserves scroll when a link with sapper-noscroll is clicked', async () => { it('preserves scroll when a link with sapper-noscroll is clicked', async () => {
await page.goto(`${base}/tall-page#foo`); await r.load('/tall-page#foo');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page"][sapper-noscroll]'); await r.page.click('[href="another-tall-page"][sapper-noscroll]');
await wait(50); await r.wait();
const scrollY = await page.evaluate(() => window.scrollY); const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0); assert.ok(scrollY > 0);
}); });
it('scrolls into a deeplink on a new page', async () => { it('scrolls into a deeplink on a new page', async () => {
await page.goto(`${base}/tall-page#foo`); await r.load('/tall-page#foo');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page#bar"]'); await r.page.click('[href="another-tall-page#bar"]');
await wait(50); await r.wait();
assert.equal(await title(), 'Another tall page'); assert.equal(await r.text('h1'), 'Another tall page');
const scrollY = await page.evaluate(() => window.scrollY); const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0); assert.ok(scrollY > 0);
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,9 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use((req, res, next) => { .use((req, res, next) => {
req.hello = 'hello'; req.hello = 'hello';
res.locals = { name: 'world' }; res.locals = { name: 'world' };
@@ -15,5 +15,6 @@ polka()
title: `${req.hello} ${res.locals.name}` title: `${req.hello} ${res.locals.name}`
}) })
}) })
) );
.listen(PORT);
start(app);

View File

@@ -1,50 +1,46 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api'; import { build } from '../../../api';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
describe('session', function() { describe('session', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname }); before('start runner', async () => {
r = await new AppRunner().start(__dirname);
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('renders session props', async () => { it('renders session props', async () => {
await page.goto(`${base}/session`); await r.load('/session');
assert.equal(await title(), 'hello world'); assert.equal(await r.text('h1'), 'hello world');
await start(); await r.sapper.start();
assert.equal(await title(), 'hello world'); assert.equal(await r.text('h1'), 'hello world');
await page.click('button'); await r.page.click('button');
assert.equal(await title(), 'changed'); assert.equal(await r.text('h1'), 'changed');
}); });
it('preloads session props', async () => { it('preloads session props', async () => {
await page.goto(`${base}/preloaded`); await r.load('/preloaded');
assert.equal(await title(), 'hello world'); assert.equal(await r.text('h1'), 'hello world');
await start(); await r.sapper.start();
assert.equal(await title(), 'hello world'); assert.equal(await r.text('h1'), 'hello world');
await page.click('button'); await r.page.click('button');
assert.equal(await title(), 'changed'); assert.equal(await r.text('h1'), 'changed');
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -2,15 +2,14 @@ import sirv from 'sirv';
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env; import { start, dev } from '../../common.js';
const dev = NODE_ENV === 'development';
polka() const app = polka()
.use( .use(
'custom-basepath', 'custom-basepath',
sirv('static', { dev }), sirv('static', { dev }),
sapper.middleware() sapper.middleware()
) );
.listen(PORT, err => {
if (err) console.log('error', err); start(app);
});

View File

@@ -1,51 +1,36 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as api from '../../../api'; import * as api from '../../../api';
import { walk } from '../../utils'; import { walk } from '../../utils';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('with-basepath', function() { describe('with-basepath', function() {
this.timeout(10000); this.timeout(10000);
let runner: AppRunner; let r: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before('build app', () => api.build({ cwd: __dirname }));
await api.build({ cwd: __dirname }); before('export app', () => api.export({ cwd: __dirname, basepath: '/custom-basepath' }));
before('start runner', async () => {
await api.export({ r = await new AppRunner().start(__dirname);
cwd: __dirname,
basepath: '/custom-basepath'
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, start, page, prefetchRoutes, title } = await runner.start());
}); });
after(() => runner.end()); after(() => r && r.end());
// tests
it('serves /custom-basepath', async () => { it('serves /custom-basepath', async () => {
await page.goto(`${base}/custom-basepath`); await r.load('/custom-basepath');
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await r.text('h1'),
'Great success!' 'Great success!'
); );
}); });
it('emits a basepath message', async () => { it('emits a basepath message', async () => {
await page.goto(`${base}/custom-basepath`); await r.load('/custom-basepath');
assert.deepEqual(runner.messages, [{ assert.deepEqual(r.messages, [{
__sapper__: true, __sapper__: true,
event: 'basepath', event: 'basepath',
basepath: '/custom-basepath' basepath: '/custom-basepath'
@@ -71,35 +56,39 @@ describe('with-basepath', function() {
}); });
it('redirects on server', async () => { it('redirects on server', async () => {
await page.goto(`${base}/custom-basepath/redirect-from`); await r.load('/custom-basepath/redirect-from');
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/custom-basepath/redirect-to` `${r.base}/custom-basepath/redirect-to`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'redirected' 'redirected'
); );
}); });
it('redirects in client', async () => { it('redirects in client', async () => {
await page.goto(`${base}/custom-basepath`); await r.load('/custom-basepath');
await start(); await r.sapper.start();
await prefetchRoutes(); await r.sapper.prefetchRoutes();
await page.click('[href="redirect-from"]'); await r.page.click('[href="redirect-from"]');
await wait(50); await r.wait();
assert.equal( assert.equal(
page.url(), r.page.url(),
`${base}/custom-basepath/redirect-to` `${r.base}/custom-basepath/redirect-to`
); );
assert.equal( assert.equal(
await title(), await r.text('h1'),
'redirected' 'redirected'
); );
}); });
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -7,10 +7,9 @@ describe('with-sourcemaps-webpack', function() {
this.timeout(10000); this.timeout(10000);
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname, bundler: 'webpack' }));
await build({ cwd: __dirname, bundler: 'webpack' });
});
// tests
it('does not put sourcemap files in service worker shell', async () => { it('does not put sourcemap files in service worker shell', async () => {
const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8'); const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8');
const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1]; const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1];
@@ -23,4 +22,4 @@ describe('with-sourcemaps-webpack', function() {
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map')); const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist'); assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
}); });
}); });

View File

@@ -1,8 +1,9 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
const { PORT } = process.env; import { start } from '../../common.js';
polka() const app = polka()
.use(sapper.middleware()) .use(sapper.middleware());
.listen(PORT);
start(app);

View File

@@ -7,10 +7,9 @@ describe('with-sourcemaps', function() {
this.timeout(10000); this.timeout(10000);
// hooks // hooks
before(async () => { before('build app', () => build({ cwd: __dirname }));
await build({ cwd: __dirname });
});
// tests
it('does not put sourcemap files in service worker shell', async () => { it('does not put sourcemap files in service worker shell', async () => {
const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8'); const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8');
const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1]; const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1];
@@ -23,4 +22,4 @@ describe('with-sourcemaps', function() {
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map')); const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist'); assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
}); });
}); });