mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-17 13:14:54 +00:00
overhaul tests
This commit is contained in:
122
test/apps/AppRunner.ts
Normal file
122
test/apps/AppRunner.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as ports from 'port-authority';
|
||||
import { fork, ChildProcess } from 'child_process';
|
||||
|
||||
declare const start: () => Promise<void>;
|
||||
declare const prefetchRoutes: () => Promise<void>;
|
||||
declare const prefetch: (href: string) => Promise<void>;
|
||||
declare const goto: (href: string) => Promise<void>;
|
||||
|
||||
export class AppRunner {
|
||||
cwd: string;
|
||||
entry: string;
|
||||
port: number;
|
||||
proc: ChildProcess;
|
||||
messages: any[];
|
||||
|
||||
browser: puppeteer.Browser;
|
||||
page: puppeteer.Page;
|
||||
|
||||
constructor(cwd: string, entry: string) {
|
||||
this.cwd = cwd;
|
||||
this.entry = path.join(cwd, entry);
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.port = await ports.find(3000);
|
||||
|
||||
this.proc = fork(this.entry, [], {
|
||||
cwd: this.cwd,
|
||||
env: {
|
||||
PORT: String(this.port)
|
||||
}
|
||||
});
|
||||
|
||||
this.proc.on('message', message => {
|
||||
if (!message.__sapper__) return;
|
||||
this.messages.push(message);
|
||||
});
|
||||
|
||||
this.browser = await puppeteer.launch({ args: ['--no-sandbox'] });
|
||||
|
||||
this.page = await this.browser.newPage();
|
||||
this.page.on('console', msg => {
|
||||
const text = msg.text();
|
||||
|
||||
if (!text.startsWith('Failed to load resource')) {
|
||||
console.log(text);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
page: this.page,
|
||||
base: `http://localhost:${this.port}`,
|
||||
|
||||
// helpers
|
||||
start: () => this.page.evaluate(() => start()),
|
||||
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()),
|
||||
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href),
|
||||
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href)
|
||||
};
|
||||
}
|
||||
|
||||
capture(fn: () => any): Promise<string[]> {
|
||||
return new Promise((fulfil, reject) => {
|
||||
const requests: string[] = [];
|
||||
const pending: Set<string> = new Set();
|
||||
let done = false;
|
||||
|
||||
function handle_request(request: puppeteer.Request) {
|
||||
const url = request.url();
|
||||
requests.push(url);
|
||||
pending.add(url);
|
||||
}
|
||||
|
||||
function handle_requestfinished(request: puppeteer.Request) {
|
||||
const url = request.url();
|
||||
pending.delete(url);
|
||||
|
||||
if (done && pending.size === 0) {
|
||||
cleanup();
|
||||
fulfil(requests);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_requestfailed(request: puppeteer.Request) {
|
||||
cleanup();
|
||||
reject(new Error(`failed to fetch ${request.url()}`))
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.page.removeListener('request', handle_request);
|
||||
this.page.removeListener('requestfinished', handle_requestfinished);
|
||||
this.page.removeListener('requestfailed', handle_requestfailed);
|
||||
};
|
||||
|
||||
this.page.on('request', handle_request);
|
||||
this.page.on('requestfinished', handle_requestfinished);
|
||||
this.page.on('requestfailed', handle_requestfailed);
|
||||
|
||||
return Promise.resolve(fn()).then(() => {
|
||||
if (pending.size === 0) {
|
||||
cleanup();
|
||||
fulfil(requests);
|
||||
}
|
||||
|
||||
done = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
end() {
|
||||
return Promise.all([
|
||||
this.browser.close(),
|
||||
new Promise(fulfil => {
|
||||
this.proc.once('exit', fulfil);
|
||||
this.proc.kill();
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
270
test/apps/basics/__test__.ts
Normal file
270
test/apps/basics/__test__.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as http from 'http';
|
||||
import { build } from '../../../api';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
declare let deleted: { id: number };
|
||||
declare let el: any;
|
||||
|
||||
describe('basics', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: 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>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
const title = () => page.$eval('h1', node => node.textContent);
|
||||
|
||||
it('serves /', async () => {
|
||||
await page.goto(base);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'Great success!'
|
||||
);
|
||||
});
|
||||
|
||||
it('serves /?', async () => {
|
||||
await page.goto(`${base}?`);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'Great success!'
|
||||
);
|
||||
});
|
||||
|
||||
it('serves static route', async () => {
|
||||
await page.goto(`${base}/a`);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'a'
|
||||
);
|
||||
});
|
||||
|
||||
it('serves static route from dir/index.html file', async () => {
|
||||
await page.goto(`${base}/b`);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'b'
|
||||
);
|
||||
});
|
||||
|
||||
it('serves dynamic route', async () => {
|
||||
await page.goto(`${base}/test-slug`);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'TEST-SLUG'
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to a new page without reloading', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
const requests: string[] = await runner.capture(async () => {
|
||||
await page.click('a[href="a"]');
|
||||
});
|
||||
|
||||
assert.deepEqual(requests, []);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'a'
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates programmatically', async () => {
|
||||
await page.goto(`${base}/a`);
|
||||
await start();
|
||||
|
||||
await goto('b');
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'b'
|
||||
);
|
||||
});
|
||||
|
||||
it('prefetches programmatically', async () => {
|
||||
await page.goto(`${base}/a`);
|
||||
await start();
|
||||
|
||||
const requests = await runner.capture(() => prefetch('b'));
|
||||
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(requests[1], `${base}/b.json`);
|
||||
});
|
||||
|
||||
|
||||
// TODO equivalent test for a webpack app
|
||||
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
const req = http.get(base, res => {
|
||||
try {
|
||||
const { headers } = res;
|
||||
|
||||
assert.equal(
|
||||
headers['content-type'],
|
||||
'text/html'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
headers['cache-control'],
|
||||
'max-age=600'
|
||||
);
|
||||
|
||||
// TODO preload more than just the entry point
|
||||
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
|
||||
const link = <string>headers['link'];
|
||||
|
||||
assert.ok(regex.test(link), link);
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls a delete handler', async () => {
|
||||
await page.goto(`${base}/delete-test`);
|
||||
await start();
|
||||
|
||||
await page.click('.del');
|
||||
await page.waitForFunction(() => deleted);
|
||||
|
||||
assert.equal(await page.evaluate(() => deleted.id), 42);
|
||||
});
|
||||
|
||||
it('hydrates initial route', async () => {
|
||||
await page.goto(base);
|
||||
|
||||
await page.evaluate(() => {
|
||||
el = document.querySelector('.hydrate-test');
|
||||
});
|
||||
|
||||
await start();
|
||||
|
||||
assert.ok(await page.evaluate(() => {
|
||||
return document.querySelector('.hydrate-test') === el;
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not attempt client-side navigation to server routes', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click(`[href="ambiguous/ok.json"]`);
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.body.textContent),
|
||||
'ok'
|
||||
);
|
||||
});
|
||||
|
||||
it('allows reserved words as route names', async () => {
|
||||
await page.goto(`${base}/const`);
|
||||
await start();
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'reserved words are okay as routes'
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts value-less query string parameter on server', async () => {
|
||||
await page.goto(`${base}/echo-query?message`);
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'message: ""'
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts value-less query string parameter on client', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('a[href="echo-query?message"]')
|
||||
|
||||
assert.equal(
|
||||
await title(),
|
||||
'message: ""'
|
||||
);
|
||||
});
|
||||
|
||||
// skipped because Nightmare doesn't seem to focus the <a> correctly
|
||||
it('resets the active element after navigation', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('[href="a"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.activeElement.nodeName),
|
||||
'BODY'
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces %sapper.xxx% tags safely', async () => {
|
||||
await page.goto(`${base}/unsafe-replacement`);
|
||||
await start();
|
||||
|
||||
const html = await page.evaluate(() => document.body.innerHTML);
|
||||
assert.equal(html.indexOf('%sapper'), -1);
|
||||
});
|
||||
});
|
||||
64
test/apps/basics/rollup.config.js
Normal file
64
test/apps/basics/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/basics/src/client.js
Normal file
9
test/apps/basics/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
1
test/apps/basics/src/routes/[slug].html
Normal file
1
test/apps/basics/src/routes/[slug].html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>{params.slug.toUpperCase()}</h1>
|
||||
3
test/apps/basics/src/routes/_error.html
Normal file
3
test/apps/basics/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
1
test/apps/basics/src/routes/a.html
Normal file
1
test/apps/basics/src/routes/a.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>a</h1>
|
||||
0
test/apps/basics/src/routes/ambiguous/[slug].html
Normal file
0
test/apps/basics/src/routes/ambiguous/[slug].html
Normal file
3
test/apps/basics/src/routes/ambiguous/[slug].json.js
Normal file
3
test/apps/basics/src/routes/ambiguous/[slug].json.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function get(req, res) {
|
||||
res.end(req.params.slug);
|
||||
}
|
||||
11
test/apps/basics/src/routes/b/index.html
Normal file
11
test/apps/basics/src/routes/b/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{letter}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return this.fetch('b.json').then(r => r.json()).then(letter => {
|
||||
return { letter };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
3
test/apps/basics/src/routes/b/index.json.js
Normal file
3
test/apps/basics/src/routes/b/index.json.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function get(req, res) {
|
||||
res.end(JSON.stringify('b'));
|
||||
}
|
||||
1
test/apps/basics/src/routes/const.html
Normal file
1
test/apps/basics/src/routes/const.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>reserved words are okay as routes</h1>
|
||||
9
test/apps/basics/src/routes/delete-test/[id].json.js
Normal file
9
test/apps/basics/src/routes/delete-test/[id].json.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function del(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
id: req.params.id
|
||||
}));
|
||||
}
|
||||
19
test/apps/basics/src/routes/delete-test/index.html
Normal file
19
test/apps/basics/src/routes/delete-test/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<button class='del' on:click='del()'>delete</button>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
oncreate() {
|
||||
window.deleted = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
del() {
|
||||
fetch(`delete-test/42.json`, { method: 'DELETE' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
window.deleted = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/apps/basics/src/routes/echo-query/index.html
Normal file
9
test/apps/basics/src/routes/echo-query/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<h1>message: "{message}"</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ query }) {
|
||||
return query;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
7
test/apps/basics/src/routes/index.html
Normal file
7
test/apps/basics/src/routes/index.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="a">a</a>
|
||||
<a href="ambiguous/ok.json">ok</a>
|
||||
<a href="echo-query?message">ok</a>
|
||||
|
||||
<div class='hydrate-test'></div>
|
||||
9
test/apps/basics/src/routes/unsafe-replacement.html
Normal file
9
test/apps/basics/src/routes/unsafe-replacement.html
Normal file
@@ -0,0 +1,9 @@
|
||||
$&
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return '$&';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
8
test/apps/basics/src/server.js
Normal file
8
test/apps/basics/src/server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
polka()
|
||||
.use(sapper.middleware())
|
||||
.listen(PORT);
|
||||
82
test/apps/basics/src/service-worker.js
Normal file
82
test/apps/basics/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/basics/src/template.html
Normal file
14
test/apps/basics/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
83
test/apps/credentials/__test__.ts
Normal file
83
test/apps/credentials/__test__.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { wait } from '../../utils';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
|
||||
describe('credentials', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// helpers
|
||||
let start: () => Promise<void>;
|
||||
let prefetchRoutes: () => Promise<void>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start, prefetchRoutes } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
it('sends cookies when using this.fetch with credentials: "include"', async () => {
|
||||
await page.goto(`${base}/credentials?creds=include`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'a: 1, b: 2, max-age: undefined'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not send cookies when using this.fetch without credentials', async () => {
|
||||
await page.goto(`${base}/credentials`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('delegates to fetch on the client', async () => {
|
||||
await page.goto(base)
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('[href="credentials?creds=include"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'a: 1, b: 2, max-age: undefined'
|
||||
);
|
||||
});
|
||||
});
|
||||
64
test/apps/credentials/rollup.config.js
Normal file
64
test/apps/credentials/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka', 'cookie'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/credentials/src/client.js
Normal file
9
test/apps/credentials/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
3
test/apps/credentials/src/routes/_error.html
Normal file
3
test/apps/credentials/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
11
test/apps/credentials/src/routes/credentials/index.html
Normal file
11
test/apps/credentials/src/routes/credentials/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{message}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ query }) {
|
||||
return this.fetch(`credentials/test.json`, {
|
||||
credentials: query.creds
|
||||
}).then(r => r.json());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
23
test/apps/credentials/src/routes/credentials/test.json.js
Normal file
23
test/apps/credentials/src/routes/credentials/test.json.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import cookie from 'cookie';
|
||||
|
||||
export function get(req, res) {
|
||||
if (req.headers.cookie) {
|
||||
const cookies = cookie.parse(req.headers.cookie);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: `a: ${cookies.a}, b: ${cookies.b}, max-age: ${cookies['max-age']}`
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(403, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: 'unauthorized'
|
||||
}));
|
||||
}
|
||||
}
|
||||
3
test/apps/credentials/src/routes/index.html
Normal file
3
test/apps/credentials/src/routes/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="credentials?creds=include">link</a>
|
||||
13
test/apps/credentials/src/server.js
Normal file
13
test/apps/credentials/src/server.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
polka()
|
||||
.use((req, res, next) => {
|
||||
// set test cookie
|
||||
res.setHeader('Set-Cookie', ['a=1; Max-Age=3600', 'b=2; Max-Age=3600']);
|
||||
next();
|
||||
})
|
||||
.use(sapper.middleware())
|
||||
.listen(PORT);
|
||||
82
test/apps/credentials/src/service-worker.js
Normal file
82
test/apps/credentials/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/credentials/src/template.html
Normal file
14
test/apps/credentials/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
92
test/apps/encoding/__test__.ts
Normal file
92
test/apps/encoding/__test__.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
describe('encoding', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// helpers
|
||||
let start: () => Promise<void>;
|
||||
let prefetchRoutes: () => Promise<void>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start, prefetchRoutes } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
it('encodes routes', async () => {
|
||||
await page.goto(`${base}/fünke`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
`I'm afraid I just blue myself`
|
||||
);
|
||||
});
|
||||
|
||||
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`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'encöded (hëllö wörld)'
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes req.params and req.query for client-rendered pages', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('a[href="echo/page/encöded?message=hëllö+wörld"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'encöded (hëllö wörld)'
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes req.params for server routes', async () => {
|
||||
await page.goto(`${base}/echo/server-route/encöded`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'encöded'
|
||||
);
|
||||
});
|
||||
});
|
||||
64
test/apps/encoding/rollup.config.js
Normal file
64
test/apps/encoding/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/encoding/src/client.js
Normal file
9
test/apps/encoding/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
3
test/apps/encoding/src/routes/_error.html
Normal file
3
test/apps/encoding/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
12
test/apps/encoding/src/routes/echo/page/[slug].html
Normal file
12
test/apps/encoding/src/routes/echo/page/[slug].html
Normal file
@@ -0,0 +1,12 @@
|
||||
<h1>{slug} ({message})</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
return {
|
||||
slug: params.slug,
|
||||
message: query.message
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
15
test/apps/encoding/src/routes/echo/server-route/[slug].js
Normal file
15
test/apps/encoding/src/routes/echo/server-route/[slug].js
Normal file
@@ -0,0 +1,15 @@
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
|
||||
res.end(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<h1>${req.params.slug}</h1>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
11
test/apps/encoding/src/routes/fünke.html
Normal file
11
test/apps/encoding/src/routes/fünke.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{phrase}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
|
||||
return { phrase };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/apps/encoding/src/routes/fünke.json.js
Normal file
9
test/apps/encoding/src/routes/fünke.json.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify(
|
||||
"I'm afraid I just blue myself"
|
||||
));
|
||||
}
|
||||
3
test/apps/encoding/src/routes/index.html
Normal file
3
test/apps/encoding/src/routes/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="echo/page/encöded?message=hëllö+wörld">link</a>
|
||||
8
test/apps/encoding/src/server.js
Normal file
8
test/apps/encoding/src/server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
polka()
|
||||
.use(sapper.middleware())
|
||||
.listen(PORT);
|
||||
82
test/apps/encoding/src/service-worker.js
Normal file
82
test/apps/encoding/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/encoding/src/template.html
Normal file
14
test/apps/encoding/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
137
test/apps/errors/__test__.ts
Normal file
137
test/apps/errors/__test__.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
describe('errors', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// helpers
|
||||
let start: () => Promise<void>;
|
||||
let prefetchRoutes: () => Promise<void>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start, prefetchRoutes } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
it('handles missing route on server', async () => {
|
||||
await page.goto(`${base}/nope`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'404'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles missing route on client', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
|
||||
await page.click('[href="nope"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'404'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles explicit 4xx on server', async () => {
|
||||
await page.goto(`${base}/blog/nope`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'404'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles explicit 4xx on client', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('[href="blog/nope"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'404'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles error on server', async () => {
|
||||
await page.goto(`${base}/throw`);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'500'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles error on client', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('[href="throw"]');
|
||||
await wait(50);
|
||||
|
||||
assert.equal(
|
||||
await page.$eval('h1', node => node.textContent),
|
||||
'500'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not serve error page for explicit non-page errors', async () => {
|
||||
await page.goto(`${base}/nope.json`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.body.textContent),
|
||||
'nope'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not serve error page for thrown non-page errors', async () => {
|
||||
await page.goto(`${base}/throw.json`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.body.textContent),
|
||||
'oops'
|
||||
);
|
||||
});
|
||||
});
|
||||
64
test/apps/errors/rollup.config.js
Normal file
64
test/apps/errors/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/errors/src/client.js
Normal file
9
test/apps/errors/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
3
test/apps/errors/src/routes/_error.html
Normal file
3
test/apps/errors/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
19
test/apps/errors/src/routes/blog/[slug].html
Normal file
19
test/apps/errors/src/routes/blog/[slug].html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params }) {
|
||||
const { slug } = params;
|
||||
|
||||
return this.fetch(`blog/${slug}.json`).then(r => {
|
||||
return r.json().then(data => {
|
||||
if (r.status !== 200) {
|
||||
this.error(r.status, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/apps/errors/src/routes/blog/[slug].json.js
Normal file
9
test/apps/errors/src/routes/blog/[slug].json.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function get(req, res) {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: 'not found'
|
||||
}));
|
||||
}
|
||||
5
test/apps/errors/src/routes/index.html
Normal file
5
test/apps/errors/src/routes/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<h1>root</h1>
|
||||
|
||||
<a href="nope">nope</a>
|
||||
<a href="blog/nope">blog/nope</a>
|
||||
<a href="throw">throw</a>
|
||||
4
test/apps/errors/src/routes/nope.json.js
Normal file
4
test/apps/errors/src/routes/nope.json.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export function get(req, res) {
|
||||
res.writeHead(500);
|
||||
res.end('nope');
|
||||
}
|
||||
7
test/apps/errors/src/routes/throw.html
Normal file
7
test/apps/errors/src/routes/throw.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
throw new Error('nope');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
3
test/apps/errors/src/routes/throw.json.js
Normal file
3
test/apps/errors/src/routes/throw.json.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function get(req, res) {
|
||||
throw new Error('oops');
|
||||
}
|
||||
8
test/apps/errors/src/server.js
Normal file
8
test/apps/errors/src/server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
polka()
|
||||
.use(sapper.middleware())
|
||||
.listen(PORT);
|
||||
82
test/apps/errors/src/service-worker.js
Normal file
82
test/apps/errors/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/errors/src/template.html
Normal file
14
test/apps/errors/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
70
test/apps/export/__test__.ts
Normal file
70
test/apps/export/__test__.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import { walk } from '../../utils';
|
||||
import * as api from '../../../api';
|
||||
|
||||
describe('export', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const builder = api.build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
builder.on('error', reject);
|
||||
builder.on('done', () => {
|
||||
// TODO it'd be nice if build and export returned promises.
|
||||
// not sure how best to combine promise and event emitter
|
||||
const exporter = api.exporter({
|
||||
build: '__sapper__/build',
|
||||
dest: '__sapper__/export',
|
||||
static: 'static',
|
||||
basepath: '',
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
exporter.on('error', (err: Error) => {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
exporter.on('done', () => fulfil());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('crawls a site', () => {
|
||||
const files = walk('__sapper__/export');
|
||||
|
||||
const client_assets = files.filter(file => file.startsWith('client/'));
|
||||
const non_client_assets = files.filter(file => !file.startsWith('client/')).sort();
|
||||
|
||||
assert.ok(client_assets.length > 0);
|
||||
|
||||
assert.deepEqual(non_client_assets, [
|
||||
'blog.json',
|
||||
'blog/bar.json',
|
||||
'blog/bar/index.html',
|
||||
'blog/baz.json',
|
||||
'blog/baz/index.html',
|
||||
'blog/foo.json',
|
||||
'blog/foo/index.html',
|
||||
'blog/index.html',
|
||||
'global.css',
|
||||
'index.html',
|
||||
'service-worker.js'
|
||||
]);
|
||||
});
|
||||
|
||||
// TODO test timeout, basepath
|
||||
});
|
||||
64
test/apps/export/rollup.config.js
Normal file
64
test/apps/export/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/export/src/client.js
Normal file
9
test/apps/export/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
3
test/apps/export/src/routes/_error.html
Normal file
3
test/apps/export/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
11
test/apps/export/src/routes/blog/[slug].html
Normal file
11
test/apps/export/src/routes/blog/[slug].html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params }) {
|
||||
return this.fetch(`blog/${params.slug}.json`).then(r => r.json()).then(post => {
|
||||
return { post };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
19
test/apps/export/src/routes/blog/[slug].json.js
Normal file
19
test/apps/export/src/routes/blog/[slug].json.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
export function get(req, res) {
|
||||
const post = posts.find(post => post.slug === req.params.slug);
|
||||
|
||||
if (post) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify(post));
|
||||
} else {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({ message: 'not found' }));
|
||||
}
|
||||
}
|
||||
5
test/apps/export/src/routes/blog/_posts.js
Normal file
5
test/apps/export/src/routes/blog/_posts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default [
|
||||
{ slug: 'foo', title: 'once upon a foo' },
|
||||
{ slug: 'bar', title: 'a bar is born' },
|
||||
{ slug: 'baz', title: 'bazzily ever after' }
|
||||
];
|
||||
15
test/apps/export/src/routes/blog/index.html
Normal file
15
test/apps/export/src/routes/blog/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<h1>blog</h1>
|
||||
|
||||
{#each posts as post}
|
||||
<p><a href="blog/{post.slug}">{post.title}</a></p>
|
||||
{/each}
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return this.fetch('blog.json').then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/apps/export/src/routes/blog/index.json.js
Normal file
9
test/apps/export/src/routes/blog/index.json.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify(posts));
|
||||
}
|
||||
3
test/apps/export/src/routes/index.html
Normal file
3
test/apps/export/src/routes/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="blog">blog</a>
|
||||
15
test/apps/export/src/server.js
Normal file
15
test/apps/export/src/server.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import sirv from 'sirv';
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT, NODE_ENV } = process.env;
|
||||
const dev = NODE_ENV === 'development';
|
||||
|
||||
polka()
|
||||
.use(
|
||||
sirv('static', { dev }),
|
||||
sapper.middleware()
|
||||
)
|
||||
.listen(PORT, err => {
|
||||
if (err) console.log('error', err);
|
||||
});
|
||||
82
test/apps/export/src/service-worker.js
Normal file
82
test/apps/export/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/export/src/template.html
Normal file
14
test/apps/export/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
3
test/apps/export/static/global.css
Normal file
3
test/apps/export/static/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: 'Comic Sans MS';
|
||||
}
|
||||
82
test/apps/ignore/__test__.ts
Normal file
82
test/apps/ignore/__test__.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
|
||||
describe('ignore', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
it('respects `options.ignore` values (RegExp)', async () => {
|
||||
await page.goto(`${base}/foobar`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.documentElement.textContent),
|
||||
'foobar'
|
||||
);
|
||||
});
|
||||
|
||||
it('respects `options.ignore` values (String #1)', async () => {
|
||||
await page.goto(`${base}/buzz`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.documentElement.textContent),
|
||||
'buzz'
|
||||
);
|
||||
});
|
||||
|
||||
it('respects `options.ignore` values (String #2)', async () => {
|
||||
await page.goto(`${base}/fizzer`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.documentElement.textContent),
|
||||
'fizzer'
|
||||
);
|
||||
});
|
||||
|
||||
it('respects `options.ignore` values (Function)', async () => {
|
||||
await page.goto(`${base}/hello`);
|
||||
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.documentElement.textContent),
|
||||
'hello'
|
||||
);
|
||||
});
|
||||
});
|
||||
64
test/apps/ignore/rollup.config.js
Normal file
64
test/apps/ignore/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/ignore/src/client.js
Normal file
9
test/apps/ignore/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
1
test/apps/ignore/src/routes/[slug].html
Normal file
1
test/apps/ignore/src/routes/[slug].html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>{params.slug.toUpperCase()}</h1>
|
||||
3
test/apps/ignore/src/routes/_error.html
Normal file
3
test/apps/ignore/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
1
test/apps/ignore/src/routes/a.html
Normal file
1
test/apps/ignore/src/routes/a.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>a</h1>
|
||||
11
test/apps/ignore/src/routes/b/index.html
Normal file
11
test/apps/ignore/src/routes/b/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{letter}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return this.fetch('b.json').then(r => r.json()).then(letter => {
|
||||
return { letter };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
3
test/apps/ignore/src/routes/b/index.json.js
Normal file
3
test/apps/ignore/src/routes/b/index.json.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function get(req, res) {
|
||||
res.end(JSON.stringify('b'));
|
||||
}
|
||||
3
test/apps/ignore/src/routes/index.html
Normal file
3
test/apps/ignore/src/routes/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="a">a</a>
|
||||
19
test/apps/ignore/src/server.js
Normal file
19
test/apps/ignore/src/server.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
const app = polka().use(sapper.middleware({
|
||||
ignore: [
|
||||
/foobar/i,
|
||||
'/buzz',
|
||||
'fizz',
|
||||
x => x === '/hello'
|
||||
]
|
||||
}));
|
||||
|
||||
['foobar', 'buzz', 'fizzer', 'hello'].forEach(uri => {
|
||||
app.get('/'+uri, (req, res) => res.end(uri));
|
||||
});
|
||||
|
||||
app.listen(PORT);
|
||||
82
test/apps/ignore/src/service-worker.js
Normal file
82
test/apps/ignore/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/ignore/src/template.html
Normal file
14
test/apps/ignore/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
74
test/apps/layout/__test__.ts
Normal file
74
test/apps/layout/__test__.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
import { wait } from '../../utils';
|
||||
|
||||
describe('layout', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// helpers
|
||||
let start: () => Promise<void>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
it('only recreates components when necessary', async () => {
|
||||
await page.goto(`${base}/foo/bar/baz`);
|
||||
await start();
|
||||
|
||||
const text1 = await page.evaluate(() => document.querySelector('#sapper').textContent);
|
||||
assert.deepEqual(text1.split('\n').filter(Boolean), [
|
||||
'y: bar 1',
|
||||
'z: baz 1',
|
||||
'click me',
|
||||
'child segment: baz'
|
||||
]);
|
||||
|
||||
await page.click('[href="foo/bar/qux"]');
|
||||
await wait(50);
|
||||
|
||||
const text2 = await page.evaluate(() => document.querySelector('#sapper').textContent);
|
||||
assert.deepEqual(text2.split('\n').filter(Boolean), [
|
||||
'y: bar 1',
|
||||
'z: qux 2',
|
||||
'click me',
|
||||
'child segment: qux'
|
||||
]);
|
||||
});
|
||||
});
|
||||
64
test/apps/layout/rollup.config.js
Normal file
64
test/apps/layout/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/layout/src/client.js
Normal file
9
test/apps/layout/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
1
test/apps/layout/src/routes/[slug].html
Normal file
1
test/apps/layout/src/routes/[slug].html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>{params.slug.toUpperCase()}</h1>
|
||||
20
test/apps/layout/src/routes/[x]/[y]/[z].html
Normal file
20
test/apps/layout/src/routes/[x]/[y]/[z].html
Normal file
@@ -0,0 +1,20 @@
|
||||
<span>z: {segment} {count}</span>
|
||||
<a href="foo/bar/qux">click me</a>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.z += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.z
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
22
test/apps/layout/src/routes/[x]/[y]/_layout.html
Normal file
22
test/apps/layout/src/routes/[x]/[y]/_layout.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<span>y: {segment} {count}</span>
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
|
||||
<span>child segment: {child.segment}</span>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.y += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.y
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
5
test/apps/layout/src/routes/[x]/_counts.js
Normal file
5
test/apps/layout/src/routes/[x]/_counts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
x: process.browser ? 1 : 0,
|
||||
y: process.browser ? 1 : 0,
|
||||
z: process.browser ? 1 : 0
|
||||
};
|
||||
3
test/apps/layout/src/routes/_error.html
Normal file
3
test/apps/layout/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
1
test/apps/layout/src/routes/index.html
Normal file
1
test/apps/layout/src/routes/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Great success!</h1>
|
||||
8
test/apps/layout/src/server.js
Normal file
8
test/apps/layout/src/server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import polka from 'polka';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
const { PORT } = process.env;
|
||||
|
||||
polka()
|
||||
.use(sapper.middleware())
|
||||
.listen(PORT);
|
||||
82
test/apps/layout/src/service-worker.js
Normal file
82
test/apps/layout/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(ASSETS);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
14
test/apps/layout/src/template.html
Normal file
14
test/apps/layout/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
109
test/apps/preloading/__test__.ts
Normal file
109
test/apps/preloading/__test__.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as path from 'path';
|
||||
import * as assert from 'assert';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { build } from '../../../api';
|
||||
import { wait } from '../../utils';
|
||||
import { AppRunner } from '../AppRunner';
|
||||
|
||||
declare const fulfil: () => Promise<void>;
|
||||
|
||||
describe('preloading', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let runner: AppRunner;
|
||||
let page: puppeteer.Page;
|
||||
let base: string;
|
||||
|
||||
// helpers
|
||||
let start: () => Promise<void>;
|
||||
let prefetchRoutes: () => Promise<void>;
|
||||
|
||||
// hooks
|
||||
before(() => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
// TODO this is brittle. Make it unnecessary
|
||||
process.chdir(__dirname);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// TODO this API isn't great. Rethink it
|
||||
const emitter = build({
|
||||
bundler: 'rollup'
|
||||
}, {
|
||||
src: path.join(__dirname, 'src'),
|
||||
routes: path.join(__dirname, 'src/routes'),
|
||||
dest: path.join(__dirname, '__sapper__/build')
|
||||
});
|
||||
|
||||
emitter.on('error', reject);
|
||||
|
||||
emitter.on('done', async () => {
|
||||
try {
|
||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||
({ base, page, start, prefetchRoutes } = await runner.start());
|
||||
|
||||
fulfil();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => runner.end());
|
||||
|
||||
const title = () => page.$eval('h1', node => node.textContent);
|
||||
|
||||
it('serializes Set objects returned from preload', async () => {
|
||||
await page.goto(`${base}/preload-values/set`);
|
||||
|
||||
assert.equal(await title(), 'true');
|
||||
|
||||
await start();
|
||||
assert.equal(await title(), 'true');
|
||||
});
|
||||
|
||||
it('bails on custom classes returned from preload', async () => {
|
||||
await page.goto(`${base}/preload-values/custom-class`);
|
||||
|
||||
assert.equal(await title(), '42');
|
||||
|
||||
await start();
|
||||
assert.equal(await title(), '42');
|
||||
});
|
||||
|
||||
it('sets preloading true when appropriate', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('a[href="slow-preload"]');
|
||||
|
||||
assert.ok(await page.evaluate(() => !!document.querySelector('progress')));
|
||||
|
||||
await page.evaluate(() => fulfil());
|
||||
assert.ok(await page.evaluate(() => !document.querySelector('progress')));
|
||||
});
|
||||
|
||||
it('runs preload in root component', async () => {
|
||||
await page.goto(`${base}/preload-root`);
|
||||
assert.equal(await title(), 'root preload function ran: true');
|
||||
});
|
||||
|
||||
it('cancels navigation if subsequent navigation occurs during preload', async () => {
|
||||
await page.goto(base);
|
||||
await start();
|
||||
await prefetchRoutes();
|
||||
|
||||
await page.click('a[href="slow-preload"]');
|
||||
await wait(100);
|
||||
await page.click('a[href="foo"]');
|
||||
|
||||
assert.equal(page.url(), `${base}/foo`);
|
||||
assert.equal(await title(), 'foo');
|
||||
|
||||
await page.evaluate(() => fulfil());
|
||||
await wait(100);
|
||||
assert.equal(page.url(), `${base}/foo`);
|
||||
assert.equal(await title(), 'foo');
|
||||
});
|
||||
});
|
||||
64
test/apps/preloading/rollup.config.js
Normal file
64
test/apps/preloading/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const dev = mode === 'development';
|
||||
|
||||
const config = require('../../../config/rollup.js');
|
||||
|
||||
export default {
|
||||
client: {
|
||||
input: config.client.input(),
|
||||
output: config.client.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
dev,
|
||||
hydratable: true,
|
||||
emitCss: true
|
||||
}),
|
||||
resolve()
|
||||
],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': false,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
}),
|
||||
svelte({
|
||||
generate: 'ssr',
|
||||
dev
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
})
|
||||
],
|
||||
external: ['sirv', 'polka'],
|
||||
|
||||
// temporary, pending Rollup 1.0
|
||||
experimentalCodeSplitting: true
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: config.serviceworker.input(),
|
||||
output: config.serviceworker.output(),
|
||||
plugins: [
|
||||
resolve(),
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
9
test/apps/preloading/src/client.js
Normal file
9
test/apps/preloading/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.start = () => sapper.start({
|
||||
target: document.querySelector('#sapper')
|
||||
});
|
||||
|
||||
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||
window.prefetch = href => sapper.prefetch(href);
|
||||
window.goto = href => sapper.goto(href);
|
||||
3
test/apps/preloading/src/routes/_error.html
Normal file
3
test/apps/preloading/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
15
test/apps/preloading/src/routes/_layout.html
Normal file
15
test/apps/preloading/src/routes/_layout.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{#if preloading}
|
||||
<progress class='preloading-progress' value=0.5/>
|
||||
{/if}
|
||||
|
||||
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
rootPreloadFunctionRan: true
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1
test/apps/preloading/src/routes/foo.html
Normal file
1
test/apps/preloading/src/routes/foo.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>foo</h1>
|
||||
4
test/apps/preloading/src/routes/index.html
Normal file
4
test/apps/preloading/src/routes/index.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="slow-preload">slow preload</a>
|
||||
<a href="foo">foo</a>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user