Added frontend tests

This commit is contained in:
Sebastian Seedorf
2020-11-17 00:06:10 +01:00
parent 37c34f99ac
commit b79c2f96cd
16 changed files with 2712 additions and 84 deletions

View File

@@ -3,8 +3,10 @@ export {setConfig} from './utils/utils';
import {getUserName} from './SomeModule';
getUserName().then((name) => {
function updateUserName(name: string): void {
const node = document.createElement('span');
node.innerText = `This user name is fetched with Javascript: ${name}`;
document.getElementsByTagName("body")[0].appendChild(node);
});
}
getUserName().then(updateUserName);

View File

@@ -5,21 +5,27 @@ enum ResolvableState {
DONE
}
class FetchOnce<T> {
class FetchOnce<T, U extends Array<unknown>> {
protected data: T|undefined;
protected error: unknown|undefined;
protected state: ResolvableState = ResolvableState.WAITING;
protected pendings: [(res: Promise<T>|T) => void, (reason: unknown) => void][] = [];
constructor(protected fetchMethod?: () => Promise<T>) { }
constructor(protected fetchMethod?: (...args: U) => Promise<T>) { }
public resolve(): Promise<T> {
public reset(): void {
this.data = undefined;
this.error = undefined;
this.state = ResolvableState.WAITING;
}
public resolve(...args: U): Promise<T> {
return new Promise((resolve, reject) => {
switch (this.state) {
case ResolvableState.WAITING:
this.state = ResolvableState.PENDING;
this.pendings.push([resolve, reject]);
if (this.fetchMethod) this.parsePromise(this.fetchMethod());
if (this.fetchMethod) this.parsePromise(this.fetchMethod(...args));
break;
case ResolvableState.PENDING:
this.pendings.push([resolve, reject]);
@@ -51,13 +57,13 @@ class FetchOnce<T> {
}
}
export class Resolvable<T> extends FetchOnce<T> {
constructor(fetchMethod: () => Promise<T>) {
export class Resolvable<T, U extends Array<unknown>> extends FetchOnce<T, U> {
constructor(fetchMethod: (...args: U) => Promise<T>) {
super(fetchMethod);
}
}
export class WaitForSync<T> extends FetchOnce<T> {
export class WaitForSync<T> extends FetchOnce<T, never> {
protected state: ResolvableState = ResolvableState.PENDING;
constructor() {
@@ -75,4 +81,9 @@ export class WaitForSync<T> extends FetchOnce<T> {
this.parsePromise((async () => { throw error; })());
}
}
public reset(): void {
super.reset();
this.state = ResolvableState.PENDING;
}
}

View File

@@ -16,8 +16,23 @@ export function getConfig(): Promise<ClientConfig> {
return configWaiter.resolve();
}
export async function getUserInfo(): Promise<UserInfo|undefined> {
export function resetConfig(): void {
return configWaiter.reset();
}
export async function getUserInfo(): Promise<Partial<UserInfo>|undefined> {
const config = await getConfig();
const res = await fetch(config.EXTERNAL_BASE_URL + "/api/user");
return res.json();
}
export type UserInfo = {
email: string,
email_verified: boolean,
family_name: string,
given_name: string,
groups: string[],
name: string,
preferred_username: string,
sub: string,
};

View File

@@ -0,0 +1,57 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as fetchMock from 'fetch-mock';
import {getUserName} from '../src/SomeModule';
import {setConfig, UserInfo} from '../src/utils/utils';
describe('frontend:SomeModule', () => {
const CONFIG = {EXTERNAL_BASE_URL: "http://demo.url"};
before(() => {
setConfig(CONFIG);
fetchMock.config.overwriteRoutes = true;
});
it('should return username', async () => {
const tests: [Partial<UserInfo>, string][] = [
[
{name: "John Doe"},
"John Doe",
], [
{name: "John Doe", email: "some.mail@example.com"},
"John Doe",
], [
{name: "", email: "some.mail@example.com"},
"",
],
];
for (const [USER_INFO, RESULT] of tests) {
fetchMock.mock('http://demo.url/api/user', {
status: 200,
body: JSON.stringify(USER_INFO),
});
const name = await getUserName();
expect(name).to.deep.equal(RESULT);
}
});
it('should return default string', async () => {
const RESULT = "No name found!";
const tests: (Partial<UserInfo>|unknown)[] = [
// eslint-disable-next-line no-null/no-null
{name: null},
// eslint-disable-next-line no-null/no-null
null,
{email: "some.mail@example.com"},
{name: undefined},
{},
];
for (const USER_INFO of tests) {
fetchMock.mock('http://demo.url/api/user', {
status: 200,
body: JSON.stringify(USER_INFO),
});
const name = await getUserName();
expect(name).to.deep.equal(RESULT);
}
});
});

View File

@@ -0,0 +1,43 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import {JSDOM} from 'jsdom';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as rewire from 'rewire';
describe("frontend:index", () => {
const updateUserName = rewire("../src/index").__get__("updateUserName") as (name: string) => void;
beforeEach(() => {
const dom = new JSDOM(
`
<html>
<body>
</body>
</html>
`,
{url: 'http://localhost'},
);
// noinspection JSConstantReassignment
global.window = dom.window;
// noinspection JSConstantReassignment
global.document = dom.window.document;
});
it("updateUserName", (done) => {
const NAME = "Patrick Star";
const RESULT = "This user name is fetched with Javascript: Patrick Star";
updateUserName(NAME);
// give the browser a chance to update the DOM
setTimeout(() => {
const span = document.getElementsByTagName("span");
expect(span).to.not.be.null;
expect(span.length).to.be.greaterThan(0);
expect(span[0].innerText).to.deep.equal(RESULT);
done();
}, 5);
});
});

View File

@@ -0,0 +1,123 @@
import {setConfig, getConfig, resetConfig, getUserInfo, UserInfo} from '../src/utils/utils';
import {Resolvable, WaitForSync} from '../src/utils/resolvable';
import {describe, it} from 'mocha';
import {expect} from 'chai';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as fetchMock from 'fetch-mock';
describe('frontend:utils - setConfig/getConfig', () => {
const CONFIG = {EXTERNAL_BASE_URL: "http://demo.url"};
afterEach(() => resetConfig());
it('should return config (afterwards)', async () => {
setConfig(CONFIG);
const config = await getConfig();
expect(config).to.deep.equal(CONFIG);
});
it('should return config (beforehand)', (done) => {
getConfig().then((config) => {
expect(config).to.deep.equal(CONFIG);
done();
});
setConfig(CONFIG);
});
});
describe('frontend:utils - getUserInfo', () => {
const CONFIG = {EXTERNAL_BASE_URL: "http://demo.url"};
const USER_INFO: Partial<UserInfo> = {
name: "John Doe",
};
beforeEach(() => setConfig(CONFIG));
afterEach(() => resetConfig());
it('should equal fetched', async () => {
setConfig(CONFIG);
fetchMock.mock('http://demo.url/api/user', {
status: 200,
body: JSON.stringify(USER_INFO),
});
const userInfo = await getUserInfo();
expect(userInfo).to.deep.equal(USER_INFO);
});
});
describe('frontend:utils - resolvable', () => {
const DATA = 5;
const ERROR = new Error("Custom error!");
it('waitForSync should return data (afterwards)', async () => {
const resolvable = new WaitForSync<number>();
resolvable.setData(DATA);
const data = await resolvable.resolve();
expect(data).to.deep.equal(DATA);
});
it('waitForSync should return data (beforehand)', (done) => {
const resolvable = new WaitForSync<number>();
resolvable.resolve().then((data) => {
expect(data).to.deep.equal(DATA);
done();
});
resolvable.setData(DATA);
});
it('waitForSync should error (afterwards)', async () => {
const resolvable = new WaitForSync<number>();
resolvable.setError(ERROR);
try {
await resolvable.resolve();
} catch (err) {
expect(err).to.deep.equal(ERROR);
}
});
it('waitForSync should error (beforehand)', (done) => {
const resolvable = new WaitForSync<number>();
resolvable.resolve().catch((err) => {
expect(err).to.deep.equal(ERROR);
done();
});
resolvable.setError(ERROR);
});
it('waitForSync should resolve data twice', async () => {
const resolvable = new WaitForSync<number>();
resolvable.setData(DATA);
const data1 = await resolvable.resolve();
const data2 = await resolvable.resolve();
expect(data1).to.deep.equal(DATA);
expect(data2).to.deep.equal(DATA);
});
it('waitForSync should error twice', async () => {
const resolvable = new WaitForSync<number>();
resolvable.setError(ERROR);
try {
await resolvable.resolve();
} catch (err) {
expect(err).to.deep.equal(ERROR);
}
try {
await resolvable.resolve();
} catch (err) {
expect(err).to.deep.equal(ERROR);
}
});
it('waitForSync should wait for resolution twice', (done) => {
const resolvable = new WaitForSync<number>();
Promise.all([
resolvable.resolve().then((data) => {
expect(data).to.deep.equal(DATA);
}), resolvable.resolve().then((data) => {
expect(data).to.deep.equal(DATA);
}),
]).then(() => done());
resolvable.setData(DATA);
});
it('resolvable should resolve', async () => {
const resolvable = new Resolvable(async () => DATA);
const data = await resolvable.resolve();
expect(data).to.deep.equal(DATA);
});
});

View File

@@ -1,10 +0,0 @@
type UserInfo = {
email: string,
email_verified: boolean,
family_name: string,
given_name: string,
groups: string[],
name: string,
preferred_username: string,
sub: string,
};