mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-16 12:54:38 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f0d94595 | ||
|
|
8155df2e22 | ||
|
|
bb51470004 | ||
|
|
53446e2ec7 | ||
|
|
c4c09550eb | ||
|
|
da47fdec96 | ||
|
|
971342ac7a | ||
|
|
3becc1cbe2 | ||
|
|
8ee5346900 | ||
|
|
9e4b79c6ff | ||
|
|
4ec1c65395 |
@@ -1,5 +1,10 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
* Each app has a single `<App>` component. See the [migration guide](https://sapper.svelte.technology/guide#0-11-to-0-12) for more information ([#157](https://github.com/sveltejs/sapper/issues/157))
|
||||||
|
* Process exits with error code 1 if build/export fails ([#208](https://github.com/sveltejs/sapper/issues/208))
|
||||||
|
|
||||||
## 0.11.1
|
## 0.11.1
|
||||||
|
|
||||||
* Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252))
|
* Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.11.1",
|
"version": "0.12.0",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "dist/middleware.ts.js",
|
"main": "dist/middleware.ts.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ prog.command('build [dest]')
|
|||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`node ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`node ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ prog.command('export [dest]')
|
|||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,10 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
|
|||||||
return !found;
|
return !found;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const max = Math.max(a.parts.length, b.parts.length);
|
if (a.parts[0] === '4xx' || a.parts[0] === '5xx') return -1;
|
||||||
|
if (b.parts[0] === '4xx' || b.parts[0] === '5xx') return 1;
|
||||||
|
|
||||||
if (max === 1) {
|
const max = Math.max(a.parts.length, b.parts.length);
|
||||||
if (a.parts[0] === '4xx' || a.parts[0] === '5xx') return -1;
|
|
||||||
if (b.parts[0] === '4xx' || b.parts[0] === '5xx') return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < max; i += 1) {
|
for (let i = 0; i < max; i += 1) {
|
||||||
const a_part = a.parts[i];
|
const a_part = a.parts[i];
|
||||||
|
|||||||
@@ -20,14 +20,7 @@ type RouteObject = {
|
|||||||
type: 'page' | 'route';
|
type: 'page' | 'route';
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
module: {
|
module: Component;
|
||||||
render: (data: any, opts: { store: Store }) => {
|
|
||||||
head: string;
|
|
||||||
css: { code: string, map: any };
|
|
||||||
html: string
|
|
||||||
},
|
|
||||||
preload: (data: any) => any | Promise<any>
|
|
||||||
};
|
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +40,24 @@ interface Req extends ClientRequest {
|
|||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function middleware({ routes, store }: {
|
interface Component {
|
||||||
|
render: (data: any, opts: { store: Store }) => {
|
||||||
|
head: string;
|
||||||
|
css: { code: string, map: any };
|
||||||
|
html: string
|
||||||
|
},
|
||||||
|
preload: (data: any) => any | Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function middleware({ App, routes, store }: {
|
||||||
|
App: Component,
|
||||||
routes: RouteObject[],
|
routes: RouteObject[],
|
||||||
store: (req: Req) => Store
|
store: (req: Req) => Store
|
||||||
}) {
|
}) {
|
||||||
|
if (!App) {
|
||||||
|
throw new Error(`As of 0.12, you must supply an App component to Sapper — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
|
||||||
|
}
|
||||||
|
|
||||||
const output = locations.dest();
|
const output = locations.dest();
|
||||||
|
|
||||||
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
|
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
|
||||||
@@ -90,7 +97,7 @@ export default function middleware({ routes, store }: {
|
|||||||
cache_control: 'max-age=31536000'
|
cache_control: 'max-age=31536000'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get_route_handler(client_info.assets, routes, store)
|
get_route_handler(client_info.assets, App, routes, store)
|
||||||
].filter(Boolean));
|
].filter(Boolean));
|
||||||
|
|
||||||
return middleware;
|
return middleware;
|
||||||
@@ -135,7 +142,7 @@ function serve({ prefix, pathname, cache_control }: {
|
|||||||
|
|
||||||
const resolved = Promise.resolve();
|
const resolved = Promise.resolve();
|
||||||
|
|
||||||
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) {
|
function get_route_handler(chunks: Record<string, string>, App: Component, routes: RouteObject[], store_getter: (req: Req) => Store) {
|
||||||
const template = dev()
|
const template = dev()
|
||||||
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
||||||
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
||||||
@@ -170,7 +177,7 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
|
|||||||
res.setHeader('Link', link);
|
res.setHeader('Link', link);
|
||||||
|
|
||||||
const store = store_getter ? store_getter(req) : null;
|
const store = store_getter ? store_getter(req) : null;
|
||||||
const data = { params: req.params, query: req.query };
|
const props = { params: req.params, query: req.query, path: req.path };
|
||||||
|
|
||||||
let redirect: { statusCode: number, location: string };
|
let redirect: { statusCode: number, location: string };
|
||||||
let error: { statusCode: number, message: Error | string };
|
let error: { statusCode: number, message: Error | string };
|
||||||
@@ -240,9 +247,9 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
|
|||||||
preloaded: mod.preload && try_serialize(preloaded),
|
preloaded: mod.preload && try_serialize(preloaded),
|
||||||
store: store && try_serialize(store.get())
|
store: store && try_serialize(store.get())
|
||||||
};
|
};
|
||||||
Object.assign(data, preloaded);
|
Object.assign(props, preloaded);
|
||||||
|
|
||||||
const { html, head, css } = mod.render(data, {
|
const { html, head, css } = App.render({ Page: mod, props }, {
|
||||||
store
|
store
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Component, ComponentConstructor, Params, Query, Route, RouteData, Scrol
|
|||||||
|
|
||||||
const manifest = typeof window !== 'undefined' && window.__SAPPER__;
|
const manifest = typeof window !== 'undefined' && window.__SAPPER__;
|
||||||
|
|
||||||
|
export let App: ComponentConstructor;
|
||||||
export let component: Component;
|
export let component: Component;
|
||||||
let target: Node;
|
let target: Node;
|
||||||
let store: Store;
|
let store: Store;
|
||||||
@@ -27,10 +28,10 @@ function select_route(url: URL): Target {
|
|||||||
if (url.origin !== window.location.origin) return null;
|
if (url.origin !== window.location.origin) return null;
|
||||||
if (!url.pathname.startsWith(manifest.baseUrl)) return null;
|
if (!url.pathname.startsWith(manifest.baseUrl)) return null;
|
||||||
|
|
||||||
const pathname = url.pathname.slice(manifest.baseUrl.length);
|
const path = url.pathname.slice(manifest.baseUrl.length);
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
const match = route.pattern.exec(pathname);
|
const match = route.pattern.exec(path);
|
||||||
if (match) {
|
if (match) {
|
||||||
if (route.ignore) return null;
|
if (route.ignore) return null;
|
||||||
|
|
||||||
@@ -43,18 +44,24 @@ function select_route(url: URL): Target {
|
|||||||
query[key] = value || true;
|
query[key] = value || true;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { url, route, data: { params, query } };
|
return { url, route, props: { params, query, path } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_token: {};
|
let current_token: {};
|
||||||
|
|
||||||
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
function render(Page: ComponentConstructor, props: any, scroll: ScrollPosition, token: {}) {
|
||||||
if (current_token !== token) return;
|
if (current_token !== token) return;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
Page,
|
||||||
|
props,
|
||||||
|
preloading: false
|
||||||
|
};
|
||||||
|
|
||||||
if (component) {
|
if (component) {
|
||||||
component.destroy();
|
component.set(data);
|
||||||
} else {
|
} else {
|
||||||
// first load — remove SSR'd <head> contents
|
// first load — remove SSR'd <head> contents
|
||||||
const start = document.querySelector('#sapper-head-start');
|
const start = document.querySelector('#sapper-head-start');
|
||||||
@@ -65,33 +72,39 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
|
|||||||
detach(start);
|
detach(start);
|
||||||
detach(end);
|
detach(end);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
component = new Component({
|
component = new App({
|
||||||
target,
|
target,
|
||||||
data,
|
data,
|
||||||
store,
|
store,
|
||||||
hydrate: !component
|
hydrate: true
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (scroll) {
|
if (scroll) {
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
function prepare_route(Page: ComponentConstructor, props: RouteData) {
|
||||||
let redirect: { statusCode: number, location: string } = null;
|
let redirect: { statusCode: number, location: string } = null;
|
||||||
let error: { statusCode: number, message: Error | string } = null;
|
let error: { statusCode: number, message: Error | string } = null;
|
||||||
|
|
||||||
if (!Component.preload) {
|
if (!Page.preload) {
|
||||||
return { Component, data, redirect, error };
|
return { Page, props, redirect, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!component && manifest.preloaded) {
|
if (!component && manifest.preloaded) {
|
||||||
return { Component, data: Object.assign(data, manifest.preloaded), redirect, error };
|
return { Page, props: Object.assign(props, manifest.preloaded), redirect, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(Component.preload.call({
|
if (component) {
|
||||||
|
component.set({
|
||||||
|
preloading: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(Page.preload.call({
|
||||||
store,
|
store,
|
||||||
fetch: (url: string, opts?: any) => window.fetch(url, opts),
|
fetch: (url: string, opts?: any) => window.fetch(url, opts),
|
||||||
redirect: (statusCode: number, location: string) => {
|
redirect: (statusCode: number, location: string) => {
|
||||||
@@ -100,7 +113,7 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
|||||||
error: (statusCode: number, message: Error | string) => {
|
error: (statusCode: number, message: Error | string) => {
|
||||||
error = { statusCode, message };
|
error = { statusCode, message };
|
||||||
}
|
}
|
||||||
}, data)).catch(err => {
|
}, props)).catch(err => {
|
||||||
error = { statusCode: 500, message: err };
|
error = { statusCode: 500, message: err };
|
||||||
}).then(preloaded => {
|
}).then(preloaded => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -108,15 +121,15 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
|||||||
? errors['4xx']
|
? errors['4xx']
|
||||||
: errors['5xx'];
|
: errors['5xx'];
|
||||||
|
|
||||||
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
|
return route.load().then(({ default: Page }: { default: ComponentConstructor }) => {
|
||||||
const err = error.message instanceof Error ? error.message : new Error(error.message);
|
const err = error.message instanceof Error ? error.message : new Error(error.message);
|
||||||
Object.assign(data, { status: error.statusCode, error: err });
|
Object.assign(props, { status: error.statusCode, error: err });
|
||||||
return { Component, data, redirect: null };
|
return { Page, props, redirect: null };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(data, preloaded)
|
Object.assign(props, preloaded)
|
||||||
return { Component, data, redirect };
|
return { Page, props, redirect };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,18 +149,18 @@ function navigate(target: Target, id: number) {
|
|||||||
|
|
||||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
prefetching.promise :
|
prefetching.promise :
|
||||||
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
target.route.load().then(mod => prepare_route(mod.default, target.props));
|
||||||
|
|
||||||
prefetching = null;
|
prefetching = null;
|
||||||
|
|
||||||
const token = current_token = {};
|
const token = current_token = {};
|
||||||
|
|
||||||
return loaded.then(({ Component, data, redirect }) => {
|
return loaded.then(({ Page, props, redirect }) => {
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
return goto(redirect.location, { replaceState: true });
|
return goto(redirect.location, { replaceState: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(Component, data, scroll_history[id], token);
|
render(Page, props, scroll_history[id], token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +221,7 @@ function handle_popstate(event: PopStateEvent) {
|
|||||||
|
|
||||||
let prefetching: {
|
let prefetching: {
|
||||||
href: string;
|
href: string;
|
||||||
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
promise: Promise<{ Page: ComponentConstructor, props: any }>;
|
||||||
} = null;
|
} = null;
|
||||||
|
|
||||||
export function prefetch(href: string) {
|
export function prefetch(href: string) {
|
||||||
@@ -217,7 +230,7 @@ export function prefetch(href: string) {
|
|||||||
if (selected && (!prefetching || href !== prefetching.href)) {
|
if (selected && (!prefetching || href !== prefetching.href)) {
|
||||||
prefetching = {
|
prefetching = {
|
||||||
href,
|
href,
|
||||||
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.props))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,12 +253,17 @@ function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
|||||||
|
|
||||||
let inited: boolean;
|
let inited: boolean;
|
||||||
|
|
||||||
export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) {
|
export function init(opts: { App: ComponentConstructor, target: Node, routes: Route[], store?: (data: any) => Store }) {
|
||||||
target = _target;
|
if (opts instanceof HTMLElement) {
|
||||||
routes = _routes.filter(r => !r.error);
|
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
|
||||||
|
}
|
||||||
|
|
||||||
|
App = opts.App;
|
||||||
|
target = opts.target;
|
||||||
|
routes = opts.routes.filter(r => !r.error);
|
||||||
errors = {
|
errors = {
|
||||||
'4xx': _routes.find(r => r.error === '4xx'),
|
'4xx': opts.routes.find(r => r.error === '4xx'),
|
||||||
'5xx': _routes.find(r => r.error === '5xx')
|
'5xx': opts.routes.find(r => r.error === '5xx')
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts && opts.store) {
|
if (opts && opts.store) {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Store } from '../interfaces';
|
|||||||
export { Store };
|
export { Store };
|
||||||
export type Params = Record<string, string>;
|
export type Params = Record<string, string>;
|
||||||
export type Query = Record<string, string | true>;
|
export type Query = Record<string, string | true>;
|
||||||
export type RouteData = { params: Params, query: Query };
|
export type RouteData = { params: Params, query: Query, path: string };
|
||||||
|
|
||||||
export interface ComponentConstructor {
|
export interface ComponentConstructor {
|
||||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
||||||
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Component {
|
export interface Component {
|
||||||
@@ -30,5 +30,5 @@ export type ScrollPosition = {
|
|||||||
export type Target = {
|
export type Target = {
|
||||||
url: URL;
|
url: URL;
|
||||||
route: Route;
|
route: Route;
|
||||||
data: RouteData;
|
props: RouteData;
|
||||||
};
|
};
|
||||||
6
test/app/app/App.html
Normal file
6
test/app/app/App.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{#if preloading}
|
||||||
|
<progress class='preloading-progress' value=0.5/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<svelte:component this={Page} {...props}/>
|
||||||
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { init, prefetchRoutes } from '../../../runtime.js';
|
import { init, prefetchRoutes } from '../../../runtime.js';
|
||||||
import { Store } from 'svelte/store.js';
|
import { Store } from 'svelte/store.js';
|
||||||
import { routes } from './manifest/client.js';
|
import { routes } from './manifest/client.js';
|
||||||
|
import App from './App.html';
|
||||||
|
|
||||||
window.init = () => {
|
window.init = () => {
|
||||||
return init(document.querySelector('#sapper'), routes, {
|
return init({
|
||||||
|
target: document.querySelector('#sapper'),
|
||||||
|
App,
|
||||||
|
routes,
|
||||||
store: data => new Store(data)
|
store: data => new Store(data)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import serve from 'serve-static';
|
|||||||
import sapper from '../../../dist/middleware.ts.js';
|
import sapper from '../../../dist/middleware.ts.js';
|
||||||
import { Store } from 'svelte/store.js';
|
import { Store } from 'svelte/store.js';
|
||||||
import { routes } from './manifest/server.js';
|
import { routes } from './manifest/server.js';
|
||||||
|
import App from './App.html'
|
||||||
|
|
||||||
let pending;
|
let pending;
|
||||||
let ended;
|
let ended;
|
||||||
@@ -86,6 +87,7 @@ const middlewares = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
sapper({
|
sapper({
|
||||||
|
App,
|
||||||
routes,
|
routes,
|
||||||
store: () => {
|
store: () => {
|
||||||
return new Store({
|
return new Store({
|
||||||
|
|||||||
@@ -574,6 +574,29 @@ function run({ mode, basepath = '' }) {
|
|||||||
assert.ok(html.indexOf('service-worker.js') !== -1);
|
assert.ok(html.indexOf('service-worker.js') !== -1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets preloading true when appropriate', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(base)
|
||||||
|
.init()
|
||||||
|
.click('a[href="slow-preload"]')
|
||||||
|
.wait(100)
|
||||||
|
.evaluate(() => {
|
||||||
|
const progress = document.querySelector('progress');
|
||||||
|
return !!progress;
|
||||||
|
})
|
||||||
|
.then(hasProgressIndicator => {
|
||||||
|
assert.ok(hasProgressIndicator);
|
||||||
|
})
|
||||||
|
.then(() => nightmare.evaluate(() => window.fulfil()))
|
||||||
|
.then(() => nightmare.evaluate(() => {
|
||||||
|
const progress = document.querySelector('progress');
|
||||||
|
return !!progress;
|
||||||
|
}))
|
||||||
|
.then(hasProgressIndicator => {
|
||||||
|
assert.ok(!hasProgressIndicator);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('headers', () => {
|
describe('headers', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user