From 0477d456a2625739a3e3a9acaf48af676ae427c2 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Thu, 6 Sep 2018 00:50:01 +0200 Subject: [PATCH 1/2] Integrate typescript-snapshots-plugin (#1847) --- package-lock.json | 18 +++++++++++++----- package.json | 1 + tsconfig.json | 7 ++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b20227b4..686af8a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10226,11 +10226,8 @@ "resolved": "https://registry.npmjs.org/fluent-intl-polyfill/-/fluent-intl-polyfill-0.1.0.tgz", "integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=", "dev": true, - "dependencies": { - "intl-pluralrules": { - "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", - "from": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" - } + "requires": { + "intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" } }, "fluent-langneg": { @@ -12674,6 +12671,11 @@ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", "dev": true }, + "intl-pluralrules": { + "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", + "from": "github:projectfluent/IntlPluralRules#module", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -24534,6 +24536,12 @@ "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", "dev": true }, + "typescript-snapshots-plugin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typescript-snapshots-plugin/-/typescript-snapshots-plugin-1.2.0.tgz", + "integrity": "sha1-4rp5y0C3Vc4tUp6h5fYlMyd5T5g=", + "dev": true + }, "ua-parser-js": { "version": "0.7.18", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", diff --git a/package.json b/package.json index 5acf90d14..49fd68d73 100644 --- a/package.json +++ b/package.json @@ -238,6 +238,7 @@ "typeface-manuale": "0.0.54", "typeface-source-sans-pro": "0.0.54", "typescript": "^3.0.3", + "typescript-snapshots-plugin": "^1.2.0", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "4.12.0", "webpack-cli": "^3.0.2", diff --git a/tsconfig.json b/tsconfig.json index 3ddce16aa..54485936a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,12 @@ "noImplicitAny": true, "strictNullChecks": true, "noErrorTruncation": true, - "lib": ["dom", "es6", "esnext.asynciterable"] + "lib": ["dom", "es6", "esnext.asynciterable"], + "plugins": [ + { + "name": "typescript-snapshots-plugin" + } + ] }, "include": [ "./src/**/.*.js", From 53e548d77a83cc75a3a7ae420fe9d50efbb8e7fe Mon Sep 17 00:00:00 2001 From: Kiwi Date: Thu, 6 Sep 2018 19:07:17 +0200 Subject: [PATCH 2/2] [next] Save Comment Draft + Pym Storage (#1843) * Implement pym storage * Save comment draft + test * Apply suggestions * Use class for PymStorage implementation * Add some comments --- src/core/client/embed/Stream.ts | 3 + .../__snapshots__/withPymStorage.spec.ts.snap | 15 +++ src/core/client/embed/decorators/index.ts | 6 +- src/core/client/embed/decorators/types.ts | 4 + .../client/embed/decorators/withAutoHeight.ts | 2 +- .../client/embed/decorators/withClickEvent.ts | 2 +- .../embed/decorators/withEventEmitter.ts | 2 +- .../withIOSSafariWidthWorkaround.ts | 2 +- .../embed/decorators/withPymStorage.spec.ts | 104 ++++++++++++++++++ .../client/embed/decorators/withPymStorage.ts | 52 +++++++++ .../embed/decorators/withSetCommentID.ts | 2 +- .../framework/lib/bootstrap/TalkContext.tsx | 9 +- .../framework/lib/bootstrap/createContext.tsx | 3 + .../framework/lib/storage/InMemoryStorage.ts | 4 + .../framework/lib/storage/PymStorage.spec.ts | 100 +++++++++++++++++ .../framework/lib/storage/PymStorage.ts | 94 ++++++++++++++++ .../client/framework/lib/storage/index.ts | 1 + .../testHelpers/createFakePymStorage.ts | 21 ++++ .../client/framework/testHelpers/index.ts | 1 + .../stream/components/PostCommentForm.tsx | 8 +- .../__snapshots__/Stream.spec.tsx.snap | 2 +- .../PostCommentFormContainer.spec.tsx | 99 +++++++++++++++++ .../containers/PostCommentFormContainer.tsx | 67 +++++++++-- .../PostCommentFormContainer.spec.tsx.snap | 20 ++++ .../stream/local/initLocalState.spec.ts | 20 ++-- .../client/stream/local/initLocalState.ts | 4 +- .../__snapshots__/postComment.spec.tsx.snap | 6 +- src/core/client/stream/test/create.tsx | 59 ++++++++++ src/core/client/stream/test/loadMore.spec.tsx | 35 +----- .../client/stream/test/permalinkView.spec.tsx | 36 +----- .../test/permalinkViewAssetNotFound.spec.tsx | 36 +----- .../permalinkViewCommentNotFound.spec.tsx | 36 +----- .../client/stream/test/postComment.spec.tsx | 47 ++------ .../client/stream/test/renderReplies.spec.tsx | 36 +----- .../client/stream/test/renderStream.spec.tsx | 36 +----- .../stream/test/showAllReplies.spec.tsx | 36 +----- 36 files changed, 727 insertions(+), 283 deletions(-) create mode 100644 src/core/client/embed/decorators/__snapshots__/withPymStorage.spec.ts.snap create mode 100644 src/core/client/embed/decorators/types.ts create mode 100644 src/core/client/embed/decorators/withPymStorage.spec.ts create mode 100644 src/core/client/embed/decorators/withPymStorage.ts create mode 100644 src/core/client/framework/lib/storage/PymStorage.spec.ts create mode 100644 src/core/client/framework/lib/storage/PymStorage.ts create mode 100644 src/core/client/framework/testHelpers/createFakePymStorage.ts create mode 100644 src/core/client/stream/containers/PostCommentFormContainer.spec.tsx create mode 100644 src/core/client/stream/containers/__snapshots__/PostCommentFormContainer.spec.tsx.snap create mode 100644 src/core/client/stream/test/create.tsx diff --git a/src/core/client/embed/Stream.ts b/src/core/client/embed/Stream.ts index ae47e450e..5486ea8a3 100644 --- a/src/core/client/embed/Stream.ts +++ b/src/core/client/embed/Stream.ts @@ -7,6 +7,7 @@ import { withClickEvent, withEventEmitter, withIOSSafariWidthWorkaround, + withPymStorage, withSetCommentID, } from "./decorators"; import PymControl from "./PymControl"; @@ -29,6 +30,8 @@ export function createPymControl(config: CreatePymControlConfig) { withClickEvent, withSetCommentID, withEventEmitter(config.eventEmitter), + withPymStorage(localStorage, "localStorage"), + withPymStorage(sessionStorage, "sessionStorage"), ]; const query = qs.stringify({ diff --git a/src/core/client/embed/decorators/__snapshots__/withPymStorage.spec.ts.snap b/src/core/client/embed/decorators/__snapshots__/withPymStorage.spec.ts.snap new file mode 100644 index 000000000..6cc6e2817 --- /dev/null +++ b/src/core/client/embed/decorators/__snapshots__/withPymStorage.spec.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withPymStorage should handle handle errors 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"error\\\\\\"}\\"}]"`; + +exports[`withPymStorage should handle unknown method 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"Unknown method unknown\\\\\\"}\\"}]"`; + +exports[`withPymStorage should set, get and remove item 1`] = ` +Object { + "talkPymStorage:key": "test", +} +`; + +exports[`withPymStorage should set, get and remove item 2`] = `Object {}`; + +exports[`withPymStorage should set, get and remove item 3`] = `"[{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"1\\\\\\",\\\\\\"result\\\\\\":\\\\\\"test\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"2\\\\\\"}\\"}]"`; diff --git a/src/core/client/embed/decorators/index.ts b/src/core/client/embed/decorators/index.ts index 6e2d45195..05a4cfda9 100644 --- a/src/core/client/embed/decorators/index.ts +++ b/src/core/client/embed/decorators/index.ts @@ -1,11 +1,9 @@ -import pym from "pym.js"; - -export type CleanupCallback = () => void; -export type Decorator = (pym: pym.Parent) => CleanupCallback | void; +export { Decorator, CleanupCallback } from "./types"; export { default as withAutoHeight } from "./withAutoHeight"; export { default as withClickEvent } from "./withClickEvent"; export { default as withSetCommentID } from "./withSetCommentID"; export { default as withEventEmitter } from "./withEventEmitter"; +export { default as withPymStorage } from "./withPymStorage"; export { default as withIOSSafariWidthWorkaround, } from "./withIOSSafariWidthWorkaround"; diff --git a/src/core/client/embed/decorators/types.ts b/src/core/client/embed/decorators/types.ts new file mode 100644 index 000000000..cb2cb609d --- /dev/null +++ b/src/core/client/embed/decorators/types.ts @@ -0,0 +1,4 @@ +import pym from "pym.js"; + +export type CleanupCallback = () => void; +export type Decorator = (pym: pym.Parent) => CleanupCallback | void; diff --git a/src/core/client/embed/decorators/withAutoHeight.ts b/src/core/client/embed/decorators/withAutoHeight.ts index 21aba9d62..a7d0c9011 100644 --- a/src/core/client/embed/decorators/withAutoHeight.ts +++ b/src/core/client/embed/decorators/withAutoHeight.ts @@ -1,4 +1,4 @@ -import { Decorator } from "./"; +import { Decorator } from "./types"; const withAutoHeight: Decorator = pym => { // Resize parent iframe height when child height changes diff --git a/src/core/client/embed/decorators/withClickEvent.ts b/src/core/client/embed/decorators/withClickEvent.ts index 1049e8431..a0c167992 100644 --- a/src/core/client/embed/decorators/withClickEvent.ts +++ b/src/core/client/embed/decorators/withClickEvent.ts @@ -1,4 +1,4 @@ -import { Decorator } from "./"; +import { Decorator } from "./types"; const withClickEvent: Decorator = pym => { const handleClick = () => pym.sendMessage("click", ""); diff --git a/src/core/client/embed/decorators/withEventEmitter.ts b/src/core/client/embed/decorators/withEventEmitter.ts index f34adf482..f15813aa4 100644 --- a/src/core/client/embed/decorators/withEventEmitter.ts +++ b/src/core/client/embed/decorators/withEventEmitter.ts @@ -1,6 +1,6 @@ import { EventEmitter2 } from "eventemitter2"; -import { Decorator } from "./"; +import { Decorator } from "./types"; const withEventEmitter = (eventEmitter: EventEmitter2): Decorator => pym => { // Pass events from iframe to the event emitter. diff --git a/src/core/client/embed/decorators/withIOSSafariWidthWorkaround.ts b/src/core/client/embed/decorators/withIOSSafariWidthWorkaround.ts index 85f0581fe..9f3c43774 100644 --- a/src/core/client/embed/decorators/withIOSSafariWidthWorkaround.ts +++ b/src/core/client/embed/decorators/withIOSSafariWidthWorkaround.ts @@ -1,4 +1,4 @@ -import { Decorator } from "./"; +import { Decorator } from "./types"; const withIOSSafariWidthWorkaround: Decorator = pym => { // Workaround: IOS Safari ignores `width` but respects `min-width` value. diff --git a/src/core/client/embed/decorators/withPymStorage.spec.ts b/src/core/client/embed/decorators/withPymStorage.spec.ts new file mode 100644 index 000000000..60ab2ed0f --- /dev/null +++ b/src/core/client/embed/decorators/withPymStorage.spec.ts @@ -0,0 +1,104 @@ +import sinon from "sinon"; + +import withPymStorage from "./withPymStorage"; + +// tslint:disable:max-classes-per-file + +class FakeStorage { + public store: Record = {}; + + public setItem(key: string, value: string) { + this.store[key] = value; + } + public removeItem(key: string) { + delete this.store[key]; + } + public getItem(key: string) { + return this.store[key]; + } +} + +class PymStub { + public listeners: Record void)> = {}; + public messages: Array<{ key: string; value: string }> = []; + public type: string; + + constructor(type: string) { + this.type = type; + } + + public onMessage(key: string, callback: (msg: string) => void) { + this.listeners[key] = callback; + } + public sendMessage(key: string, value: string) { + this.messages.push({ key, value }); + } +} + +describe("withPymStorage", () => { + it("should set, get and remove item", () => { + const pym = new PymStub("localStorage"); + const storage = new FakeStorage(); + withPymStorage(storage as any, "localStorage", "talkPymStorage:")( + pym as any + ); + pym.listeners["pymStorage.localStorage.request"]( + JSON.stringify({ + id: "0", + method: "setItem", + parameters: { key: "key", value: "test" }, + }) + ); + expect(storage.store).toMatchSnapshot(); + pym.listeners["pymStorage.localStorage.request"]( + JSON.stringify({ + id: "1", + method: "getItem", + parameters: { key: "key" }, + }) + ); + pym.listeners["pymStorage.localStorage.request"]( + JSON.stringify({ + id: "2", + method: "removeItem", + parameters: { key: "key" }, + }) + ); + expect(storage.store).toMatchSnapshot(); + expect(JSON.stringify(pym.messages)).toMatchSnapshot(); + }); + it("should handle unknown method", () => { + const pym = new PymStub("localStorage"); + const storage = new FakeStorage(); + withPymStorage(storage as any, "localStorage", "talkPymStorage:")( + pym as any + ); + pym.listeners["pymStorage.localStorage.request"]( + JSON.stringify({ + id: "0", + method: "unknown", + parameters: {}, + }) + ); + expect(JSON.stringify(pym.messages)).toMatchSnapshot(); + }); + it("should handle handle errors", () => { + const pym = new PymStub("localStorage"); + const storage = new FakeStorage(); + sinon + .mock(storage) + .expects("getItem") + .throws("error"); + withPymStorage(storage as any, "localStorage", "talkPymStorage:")( + pym as any + ); + pym.listeners["pymStorage.localStorage.request"]( + JSON.stringify({ + id: "0", + method: "getItem", + parameters: {}, + }) + ); + expect(JSON.stringify(pym.messages)).toMatchSnapshot(); + }); +}); diff --git a/src/core/client/embed/decorators/withPymStorage.ts b/src/core/client/embed/decorators/withPymStorage.ts new file mode 100644 index 000000000..2dcaff1c0 --- /dev/null +++ b/src/core/client/embed/decorators/withPymStorage.ts @@ -0,0 +1,52 @@ +import { Decorator } from "./types"; + +const withPymStorage = ( + storage: Storage, + type: "localStorage" | "sessionStorage", + prefix = "talkPymStorage:" +): Decorator => pym => { + pym.onMessage(`pymStorage.${type}.request`, (msg: any) => { + const { id, method, parameters } = JSON.parse(msg); + const { key, value } = parameters; + const prefixedKey = `${prefix}${key}`; + + // Variable for the method return value. + let result; + + const sendError = (error: string) => { + // tslint:disable-next-line:no-console + console.error(error); + pym.sendMessage( + `pymStorage.${type}.error`, + JSON.stringify({ id, error }) + ); + }; + + try { + switch (method) { + case "setItem": + result = storage.setItem(prefixedKey, value); + break; + case "getItem": + result = storage.getItem(prefixedKey); + break; + case "removeItem": + result = storage.removeItem(prefixedKey); + break; + default: + sendError(`Unknown method ${method}`); + return; + } + } catch (err) { + sendError(err.toString()); + return; + } + + pym.sendMessage( + `pymStorage.${type}.response`, + JSON.stringify({ id, result }) + ); + }); +}; + +export default withPymStorage; diff --git a/src/core/client/embed/decorators/withSetCommentID.ts b/src/core/client/embed/decorators/withSetCommentID.ts index 7e6c34be4..b7f0ddc9e 100644 --- a/src/core/client/embed/decorators/withSetCommentID.ts +++ b/src/core/client/embed/decorators/withSetCommentID.ts @@ -1,7 +1,7 @@ import qs from "query-string"; import { buildURL } from "../utils"; -import { Decorator } from "./"; +import { Decorator } from "./types"; function getCurrentCommentID() { return qs.parse(location.search).commentID; diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index a77a14576..f46bee9f2 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -8,6 +8,7 @@ import { Environment } from "relay-runtime"; import { PostMessageService } from "talk-framework/lib/postMessage"; import { RestClient } from "talk-framework/lib/rest"; +import { PymStorage } from "talk-framework/lib/storage"; import { UIContext } from "talk-ui/components"; import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside"; @@ -21,12 +22,18 @@ export interface TalkContext { /** formatter for timeago. */ timeagoFormatter?: Formatter; - /** Session Storage */ + /** Local Storage */ localStorage: Storage; /** Session storage */ sessionStorage: Storage; + /** Local Storage over pym */ + pymLocalStorage?: PymStorage; + + /** Session storage over pym */ + pymSessionStorage?: PymStorage; + /** media query values for testing purposes */ mediaQueryValues?: MediaQueryMatchers; diff --git a/src/core/client/framework/lib/bootstrap/createContext.tsx b/src/core/client/framework/lib/bootstrap/createContext.tsx index 325c2e54e..3fce90cd3 100644 --- a/src/core/client/framework/lib/bootstrap/createContext.tsx +++ b/src/core/client/framework/lib/bootstrap/createContext.tsx @@ -8,6 +8,7 @@ import { Environment, Network, RecordSource, Store } from "relay-runtime"; import { LOCAL_ID } from "talk-framework/lib/relay"; import { createLocalStorage, + createPymStorage, createSessionStorage, } from "talk-framework/lib/storage"; @@ -118,6 +119,8 @@ export default async function createContext({ postMessage: new PostMessageService(), localStorage: createLocalStorage(), sessionStorage: createSessionStorage(), + pymLocalStorage: pym && createPymStorage(pym, "localStorage"), + pymSessionStorage: pym && createPymStorage(pym, "sessionStorage"), }; // Run custom initializations. diff --git a/src/core/client/framework/lib/storage/InMemoryStorage.ts b/src/core/client/framework/lib/storage/InMemoryStorage.ts index 3c5b15d7c..ec4838b20 100644 --- a/src/core/client/framework/lib/storage/InMemoryStorage.ts +++ b/src/core/client/framework/lib/storage/InMemoryStorage.ts @@ -38,6 +38,10 @@ class InMemoryStorage implements Storage { public removeItem(key: string) { delete this.storage[key]; } + + public toString() { + return JSON.stringify(this.storage); + } } export default function createInMemoryStorage() { diff --git a/src/core/client/framework/lib/storage/PymStorage.spec.ts b/src/core/client/framework/lib/storage/PymStorage.spec.ts new file mode 100644 index 000000000..7a9c9a805 --- /dev/null +++ b/src/core/client/framework/lib/storage/PymStorage.spec.ts @@ -0,0 +1,100 @@ +import createPymStorage from "./PymStorage"; + +class PymStub { + public listeners: Record void)> = {}; + public messages: Array<{ key: string; value: string }> = []; + public type: string; + + constructor(type: string) { + this.type = type; + } + + public onMessage(key: string, callback: (msg: string) => void) { + this.listeners[key] = callback; + } + public sendMessage(key: string, value: string) { + this.messages.push({ key, value }); + } +} + +describe("PymStorage", () => { + it("should set item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.setItem("test", "value"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id, method, parameters } = JSON.parse(value); + expect(method).toBe("setItem"); + expect(parameters).toEqual({ key: "test", value: "value" }); + pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id })); + expect(promise).resolves.toBeUndefined(); + }); + + it("should remove item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.removeItem("test"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id, method, parameters } = JSON.parse(value); + expect(method).toBe("removeItem"); + expect(parameters).toEqual({ key: "test" }); + pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id })); + expect(promise).resolves.toBeUndefined(); + }); + + it("should get item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.getItem("test"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id, method, parameters } = JSON.parse(value); + expect(method).toBe("getItem"); + expect(parameters).toEqual({ key: "test" }); + pym.listeners["pymStorage.localStorage.response"]( + JSON.stringify({ id, result: "value" }) + ); + expect(promise).resolves.toBe("value"); + }); + + describe("on error", () => { + it("should reject set item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.setItem("test", "value"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id } = JSON.parse(value); + pym.listeners["pymStorage.localStorage.error"]( + JSON.stringify({ id, error: "error" }) + ); + expect(promise).rejects.toThrow(new Error("error")); + }); + it("should reject remove item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.removeItem("test"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id } = JSON.parse(value); + pym.listeners["pymStorage.localStorage.error"]( + JSON.stringify({ id, error: "error" }) + ); + expect(promise).rejects.toThrow(new Error("error")); + }); + it("should reject get item", () => { + const pym = new PymStub("localStorage"); + const storage = createPymStorage(pym as any, "localStorage"); + const promise = storage.getItem("test"); + const { key, value } = pym.messages.pop()!; + expect(key).toBe(`pymStorage.localStorage.request`); + const { id } = JSON.parse(value); + pym.listeners["pymStorage.localStorage.error"]( + JSON.stringify({ id, error: "error" }) + ); + expect(promise).rejects.toThrow(new Error("error")); + }); + }); +}); diff --git a/src/core/client/framework/lib/storage/PymStorage.ts b/src/core/client/framework/lib/storage/PymStorage.ts new file mode 100644 index 000000000..0b66f9c7f --- /dev/null +++ b/src/core/client/framework/lib/storage/PymStorage.ts @@ -0,0 +1,94 @@ +import { Child, Parent } from "pym.js"; +import uuid from "uuid/v4"; + +type Pym = Child | Parent; + +export interface PymStorage { + /** + * value = storage[key] + */ + getItem(key: string): Promise; + /** + * delete storage[key] + */ + removeItem(key: string): Promise; + /** + * storage[key] = value + */ + setItem(key: string, value: string): Promise; +} + +class PymStorageImpl implements PymStorage { + /** Instance to pym */ + private pym: Pym; + + /** Requested storage type */ + private type: string; + + /** A Map of requestID => {resolve, reject} */ + private requests: Record< + string, + { resolve: ((v: any) => void); reject: ((v: any) => void) } + > = {}; + + /** Requests method with parameters over pym. */ + private call( + method: string, + parameters: { key: string; value?: string } + ): Promise { + const id = uuid(); + return new Promise((resolve, reject) => { + this.requests[id] = { resolve, reject }; + this.pym.sendMessage( + `pymStorage.${this.type}.request`, + JSON.stringify({ id, method, parameters }) + ); + }); + } + + /** Listen to pym responses */ + private listen() { + // Receive successful responses. + this.pym.onMessage(`pymStorage.${this.type}.response`, (msg: string) => { + const { id, result } = JSON.parse(msg); + this.requests[id].resolve(result); + delete this.requests[id]; + }); + + // Receive error responses. + this.pym.onMessage(`pymStorage.${this.type}.error`, (msg: string) => { + const { id, error } = JSON.parse(msg); + this.requests[id].reject(new Error(error)); + delete this.requests[id]; + }); + } + + constructor(pym: Pym, type: string) { + this.pym = pym; + this.type = type; + this.listen(); + } + + public setItem(key: string, value: string) { + return this.call("setItem", { key, value }); + } + public getItem(key: string) { + return this.call("getItem", { key }); + } + public removeItem(key: string) { + return this.call("removeItem", { key }); + } +} + +/** + * Creates a storage that put requests onto pym. + * This is the counterpart of `connectStorageToPym`. + * @param {string} pym pym + * @return {Object} storage + */ +export default function createPymStorage( + pym: Pym, + type: "localStorage" | "sessionStorage" +): PymStorage { + return new PymStorageImpl(pym, type); +} diff --git a/src/core/client/framework/lib/storage/index.ts b/src/core/client/framework/lib/storage/index.ts index a558f693b..e17d10668 100644 --- a/src/core/client/framework/lib/storage/index.ts +++ b/src/core/client/framework/lib/storage/index.ts @@ -1,3 +1,4 @@ export { default as createInMemoryStorage } from "./InMemoryStorage"; export { default as createLocalStorage } from "./LocalStorage"; export { default as createSessionStorage } from "./SessionStorage"; +export { default as createPymStorage, PymStorage } from "./PymStorage"; diff --git a/src/core/client/framework/testHelpers/createFakePymStorage.ts b/src/core/client/framework/testHelpers/createFakePymStorage.ts new file mode 100644 index 000000000..ad8510dcd --- /dev/null +++ b/src/core/client/framework/testHelpers/createFakePymStorage.ts @@ -0,0 +1,21 @@ +import { PymStorage } from "talk-framework/lib/storage"; + +export class FakeStorage implements PymStorage { + public store: Record = {}; + + public setItem(key: string, value: string) { + this.store[key] = value; + return Promise.resolve(); + } + public removeItem(key: string) { + delete this.store[key]; + return Promise.resolve(); + } + public getItem(key: string) { + return Promise.resolve(this.store[key]); + } +} + +export default function createFakePymStorage() { + return new FakeStorage(); +} diff --git a/src/core/client/framework/testHelpers/index.ts b/src/core/client/framework/testHelpers/index.ts index 345e7be68..69e9c5cd0 100644 --- a/src/core/client/framework/testHelpers/index.ts +++ b/src/core/client/framework/testHelpers/index.ts @@ -4,6 +4,7 @@ export { } from "./createRelayEnvironment"; export { default as createFluentBundle } from "./createFluentBundle"; export { default as createSinonStub } from "./createSinonStub"; +export { default as createFakePymStorage } from "./createFakePymStorage"; export { default as removeFragmentRefs, NoFragmentRefs, diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index 00f544b86..09d4a1ba6 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -1,6 +1,7 @@ +import { FormState } from "final-form"; import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; -import { Field, Form } from "react-final-form"; +import { Field, Form, FormSpy } from "react-final-form"; import { OnSubmit } from "talk-framework/lib/form"; import { required } from "talk-framework/lib/validation"; @@ -22,10 +23,12 @@ interface FormProps { export interface PostCommentFormProps { onSubmit: OnSubmit; + onChange?: (state: FormState) => void; + initialValues?: FormProps; } const PostCommentForm: StatelessComponent = props => ( -
+ {({ handleSubmit, submitting }) => ( = props => ( className={styles.root} id="comments-postCommentForm-form" > + {({ input, meta }) => ( diff --git a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap index 46c215a5b..b022bd1e3 100644 --- a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap @@ -213,7 +213,7 @@ exports[`when use is logged in renders correctly 1`] = ` - diff --git a/src/core/client/stream/containers/PostCommentFormContainer.spec.tsx b/src/core/client/stream/containers/PostCommentFormContainer.spec.tsx new file mode 100644 index 000000000..53b104b3a --- /dev/null +++ b/src/core/client/stream/containers/PostCommentFormContainer.spec.tsx @@ -0,0 +1,99 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; +import sinon from "sinon"; + +import { PropTypesOf } from "talk-framework/types"; + +import { timeout } from "talk-common/utils"; +import { createFakePymStorage } from "talk-framework/testHelpers"; +import { PostCommentFormContainer } from "./PostCommentFormContainer"; + +const contextKey = "postCommentFormBody"; + +it("renders correctly", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + assetID: "asset-id", + pymSessionStorage: createFakePymStorage(), + }; + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders with initialValues", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + assetID: "asset-id", + pymSessionStorage: createFakePymStorage(), + }; + + await props.pymSessionStorage.setItem(contextKey, "Hello World!"); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it("save values", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + assetID: "asset-id", + pymSessionStorage: createFakePymStorage(), + }; + + await props.pymSessionStorage.setItem(contextKey, "Hello World!"); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + wrapper + .first() + .props() + .onChange({ values: { body: "changed" } }); + expect(await props.pymSessionStorage.getItem(contextKey)).toBe("changed"); +}); + +it("creates a comment", async () => { + const assetID = "asset-id"; + const input = { body: "Hello World!" }; + const createCommentStub = sinon.stub(); + const form = { reset: noop }; + const formMock = sinon.mock(form); + formMock + .expects("reset") + .withArgs({}) + .once(); + + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: createCommentStub, + assetID, + pymSessionStorage: createFakePymStorage(), + }; + + await props.pymSessionStorage.setItem(contextKey, "Hello World!"); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + wrapper + .first() + .props() + .onSubmit(input, form); + expect( + createCommentStub.calledWith({ + assetID, + ...input, + }) + ).toBeTruthy(); + await timeout(); + formMock.verify(); +}); diff --git a/src/core/client/stream/containers/PostCommentFormContainer.tsx b/src/core/client/stream/containers/PostCommentFormContainer.tsx index 6398025eb..c862ce8ad 100644 --- a/src/core/client/stream/containers/PostCommentFormContainer.tsx +++ b/src/core/client/stream/containers/PostCommentFormContainer.tsx @@ -1,6 +1,8 @@ -import React, { Component, ReactNode } from "react"; +import React, { Component } from "react"; +import { withContext } from "talk-framework/lib/bootstrap"; import { BadUserInputError } from "talk-framework/lib/errors"; +import { PymStorage } from "talk-framework/lib/storage"; import { PropTypesOf } from "talk-framework/types"; import PostCommentForm, { @@ -11,17 +13,48 @@ import { CreateCommentMutation, withCreateCommentMutation } from "../mutations"; interface InnerProps { createComment: CreateCommentMutation; assetID: string; - children?: ReactNode; + pymSessionStorage: PymStorage; } -class PostCommentFormContainer extends Component { - private onSubmit: PostCommentFormProps["onSubmit"] = async (input, form) => { +interface State { + initialValues?: PostCommentFormProps["initialValues"]; + initialized: boolean; +} + +const contextKey = "postCommentFormBody"; + +export class PostCommentFormContainer extends Component { + public state: State = { initialized: false }; + + constructor(props: InnerProps) { + super(props); + this.init(); + } + + private async init() { + const body = await this.props.pymSessionStorage.getItem(contextKey); + if (body) { + this.setState({ + initialValues: { + body, + }, + }); + } + this.setState({ + initialized: true, + }); + } + + private handleOnSubmit: PostCommentFormProps["onSubmit"] = async ( + input, + form + ) => { try { await this.props.createComment({ assetID: this.props.assetID, ...input, }); - form.reset(); + form.reset({}); } catch (error) { if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; @@ -31,11 +64,31 @@ class PostCommentFormContainer extends Component { } return undefined; }; + + private handleOnChange: PostCommentFormProps["onChange"] = state => { + if (state.values.body) { + this.props.pymSessionStorage.setItem(contextKey, state.values.body); + } else { + this.props.pymSessionStorage.removeItem(contextKey); + } + }; + public render() { - return ; + if (!this.state.initialized) { + return null; + } + return ( + + ); } } -const enhanced = withCreateCommentMutation(PostCommentFormContainer); +const enhanced = withContext(({ pymSessionStorage }) => ({ + pymSessionStorage, +}))(withCreateCommentMutation(PostCommentFormContainer)); export type PostCommentFormContainerProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/stream/containers/__snapshots__/PostCommentFormContainer.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/PostCommentFormContainer.spec.tsx.snap new file mode 100644 index 000000000..de1ba17f5 --- /dev/null +++ b/src/core/client/stream/containers/__snapshots__/PostCommentFormContainer.spec.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +`; + +exports[`renders with initialValues 1`] = ` + +`; diff --git a/src/core/client/stream/local/initLocalState.spec.ts b/src/core/client/stream/local/initLocalState.spec.ts index 2d2a3b486..49f090742 100644 --- a/src/core/client/stream/local/initLocalState.spec.ts +++ b/src/core/client/stream/local/initLocalState.spec.ts @@ -19,12 +19,14 @@ beforeEach(() => { }); it("init local state", async () => { - initLocalState(environment, { localStorage: createInMemoryStorage() } as any); + await initLocalState(environment, { + localStorage: createInMemoryStorage(), + } as any); await timeout(); expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot(); }); -it("set assetID from query", () => { +it("set assetID from query", async () => { const assetID = "asset-id"; const previousLocation = location.toString(); const previousState = window.history.state; @@ -33,12 +35,14 @@ it("set assetID from query", () => { document.title, `http://localhost/?assetID=${assetID}` ); - initLocalState(environment, { localStorage: createInMemoryStorage() } as any); + await initLocalState(environment, { + localStorage: createInMemoryStorage(), + } as any); expect(source.get(LOCAL_ID)!.assetID).toBe(assetID); window.history.replaceState(previousState, document.title, previousLocation); }); -it("set commentID from query", () => { +it("set commentID from query", async () => { const commentID = "comment-id"; const previousLocation = location.toString(); const previousState = window.history.state; @@ -47,16 +51,18 @@ it("set commentID from query", () => { document.title, `http://localhost/?commentID=${commentID}` ); - initLocalState(environment, { localStorage: createInMemoryStorage() } as any); + await initLocalState(environment, { + localStorage: createInMemoryStorage(), + } as any); expect(source.get(LOCAL_ID)!.commentID).toBe(commentID); window.history.replaceState(previousState, document.title, previousLocation); }); -it("set authToken from localStorage", () => { +it("set authToken from localStorage", async () => { const authToken = "auth-token"; const localStorage = createInMemoryStorage(); localStorage.setItem("authToken", authToken); - initLocalState(environment, { localStorage } as any); + await initLocalState(environment, { localStorage } as any); expect(source.get(LOCAL_ID)!.authToken).toBe(authToken); localStorage.removeItem("authToken"); }); diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index 81ba25702..81b32a742 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -22,6 +22,8 @@ export default async function initLocalState( environment: Environment, { localStorage }: TalkContext ) { + const authToken = await localStorage.getItem("authToken"); + commitLocalUpdate(environment, s => { // TODO: (cvle) move local, auth token and network initialization to framework. const root = s.getRoot(); @@ -31,7 +33,7 @@ export default async function initLocalState( root.setLinkedRecord(localRecord, "local"); // Set auth token - localRecord.setValue(localStorage.getItem("authToken") || "", "authToken"); + localRecord.setValue(authToken || "", "authToken"); // Set initial auth revision, this is increment whenenver auth state might have changed. localRecord.setValue(0, "authRevision"); diff --git a/src/core/client/stream/test/__snapshots__/postComment.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/postComment.spec.tsx.snap index cb4c00a5c..491f705d3 100644 --- a/src/core/client/stream/test/__snapshots__/postComment.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/postComment.spec.tsx.snap @@ -194,10 +194,10 @@ exports[`post a comment 1`] = `
; + initLocalState?: ( + local: RecordProxy, + source: RecordSourceProxy, + environment: Environment + ) => void; +} + +export default function create(params: CreateParams) { + const environment = createEnvironment({ + // Set this to true, to see graphql responses. + logNetwork: params.logNetwork, + resolvers: params.resolvers, + initLocalState: (localRecord, source, env) => { + localRecord.setValue(0, "authRevision"); + if (params.initLocalState) { + params.initLocalState(localRecord, source, env); + } + }, + }); + + const context: TalkContext = { + relayEnvironment: environment, + localeBundles: [createFluentBundle()], + localStorage: createInMemoryStorage(), + sessionStorage: createInMemoryStorage(), + pymLocalStorage: createFakePymStorage(), + pymSessionStorage: createFakePymStorage(), + rest: new RestClient("http://localhost/api"), + postMessage: new PostMessageService(), + }; + + const testRenderer = TestRenderer.create( + + + , + { createNodeMock } + ); + + return { context, testRenderer }; +} diff --git a/src/core/client/stream/test/loadMore.spec.tsx b/src/core/client/stream/test/loadMore.spec.tsx index f58a80004..ea72b5510 100644 --- a/src/core/client/stream/test/loadMore.spec.tsx +++ b/src/core/client/stream/test/loadMore.spec.tsx @@ -1,17 +1,9 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; +import { ReactTestRenderer } from "react-test-renderer"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets, comments } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -68,31 +60,14 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord, source) => { - localRecord.setValue(0, "authRevision"); + initLocalState: localRecord => { localRecord.setValue(assetStub.id, "assetID"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders comment stream", async () => { diff --git a/src/core/client/stream/test/permalinkView.spec.tsx b/src/core/client/stream/test/permalinkView.spec.tsx index c68d3fde3..438ca2cea 100644 --- a/src/core/client/stream/test/permalinkView.spec.tsx +++ b/src/core/client/stream/test/permalinkView.spec.tsx @@ -1,19 +1,10 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import sinon from "sinon"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets, comments } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -50,32 +41,15 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { - localRecord.setValue(0, "authRevision"); + initLocalState: localRecord => { localRecord.setValue(assetStub.id, "assetID"); localRecord.setValue(commentStub.id, "commentID"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders permalink view", async () => { diff --git a/src/core/client/stream/test/permalinkViewAssetNotFound.spec.tsx b/src/core/client/stream/test/permalinkViewAssetNotFound.spec.tsx index 17b23b670..9d2df883e 100644 --- a/src/core/client/stream/test/permalinkViewAssetNotFound.spec.tsx +++ b/src/core/client/stream/test/permalinkViewAssetNotFound.spec.tsx @@ -1,17 +1,8 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; let testRenderer: ReactTestRenderer; beforeEach(() => { @@ -22,32 +13,15 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { - localRecord.setValue(0, "authRevision"); + initLocalState: localRecord => { localRecord.setValue("unknown-asset-id", "assetID"); localRecord.setValue("unknown-comment-id", "commentID"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders permalink view with unknown asset", async () => { diff --git a/src/core/client/stream/test/permalinkViewCommentNotFound.spec.tsx b/src/core/client/stream/test/permalinkViewCommentNotFound.spec.tsx index f49dc0534..6a7279669 100644 --- a/src/core/client/stream/test/permalinkViewCommentNotFound.spec.tsx +++ b/src/core/client/stream/test/permalinkViewCommentNotFound.spec.tsx @@ -1,19 +1,10 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import sinon from "sinon"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets, comments } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -47,32 +38,15 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { + initLocalState: localRecord => { localRecord.setValue(assetStub.id, "assetID"); localRecord.setValue("unknown-comment-id", "commentID"); - localRecord.setValue(0, "authRevision"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders permalink view with unknown comment", async () => { diff --git a/src/core/client/stream/test/postComment.spec.tsx b/src/core/client/stream/test/postComment.spec.tsx index f4d97c398..1260e545e 100644 --- a/src/core/client/stream/test/postComment.spec.tsx +++ b/src/core/client/stream/test/postComment.spec.tsx @@ -1,19 +1,10 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import timekeeper from "timekeeper"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets, users } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -58,31 +49,14 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { + initLocalState: localRecord => { localRecord.setValue(assets[0].id, "assetID"); - localRecord.setValue(0, "authRevision"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders comment stream", async () => { @@ -92,25 +66,26 @@ it("renders comment stream", async () => { }); it("post a comment", async () => { + // Wait for loading. + await timeout(); testRenderer.root .findByProps({ inputId: "comments-postCommentForm-field" }) .props.onChange({ html: "Hello world!" }); - timekeeper.freeze(new Date("2018-07-06T18:24:00.002Z")); + timekeeper.freeze(new Date("2018-07-06T18:24:00.000Z")); + testRenderer.root .findByProps({ id: "comments-postCommentForm-form" }) .props.onSubmit(); + timekeeper.reset(); + // Test optimistic response. expect(testRenderer.toJSON()).toMatchSnapshot(); // Wait for loading. await timeout(); - // Travel to the time where the "timeout" has executed. - timekeeper.travel(new Date("2018-07-06T18:24:01.002Z")); - // Test after server response. expect(testRenderer.toJSON()).toMatchSnapshot(); - timekeeper.reset(); }); diff --git a/src/core/client/stream/test/renderReplies.spec.tsx b/src/core/client/stream/test/renderReplies.spec.tsx index 970d1a31b..b508d78d8 100644 --- a/src/core/client/stream/test/renderReplies.spec.tsx +++ b/src/core/client/stream/test/renderReplies.spec.tsx @@ -1,18 +1,9 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assetWithReplies } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -29,31 +20,14 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { + initLocalState: localRecord => { localRecord.setValue(assetWithReplies.id, "assetID"); - localRecord.setValue(0, "authRevision"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders comment stream", async () => { diff --git a/src/core/client/stream/test/renderStream.spec.tsx b/src/core/client/stream/test/renderStream.spec.tsx index 3ea0d74c6..131bb678c 100644 --- a/src/core/client/stream/test/renderStream.spec.tsx +++ b/src/core/client/stream/test/renderStream.spec.tsx @@ -1,18 +1,9 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -26,31 +17,14 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { + initLocalState: localRecord => { localRecord.setValue(assets[0].id, "assetID"); - localRecord.setValue(0, "authRevision"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders comment stream", async () => { diff --git a/src/core/client/stream/test/showAllReplies.spec.tsx b/src/core/client/stream/test/showAllReplies.spec.tsx index a76aab40d..52d302e55 100644 --- a/src/core/client/stream/test/showAllReplies.spec.tsx +++ b/src/core/client/stream/test/showAllReplies.spec.tsx @@ -1,19 +1,10 @@ -import React from "react"; -import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; -import { RecordProxy } from "relay-runtime"; +import { ReactTestRenderer } from "react-test-renderer"; import sinon from "sinon"; import { timeout } from "talk-common/utils"; -import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; -import { PostMessageService } from "talk-framework/lib/postMessage"; -import { RestClient } from "talk-framework/lib/rest"; -import { createInMemoryStorage } from "talk-framework/lib/storage"; import { createSinonStub } from "talk-framework/testHelpers"; -import AppContainer from "talk-stream/containers/AppContainer"; -import createEnvironment from "./createEnvironment"; -import createFluentBundle from "./createFluentBundle"; -import createNodeMock from "./createNodeMock"; +import create from "./create"; import { assets, comments } from "./fixtures"; let testRenderer: ReactTestRenderer; @@ -85,31 +76,14 @@ beforeEach(() => { }, }; - const environment = createEnvironment({ + ({ testRenderer } = create({ // Set this to true, to see graphql responses. logNetwork: false, resolvers, - initLocalState: (localRecord: RecordProxy) => { + initLocalState: localRecord => { localRecord.setValue(assetStub.id, "assetID"); - localRecord.setValue(0, "authRevision"); }, - }); - - const context: TalkContext = { - relayEnvironment: environment, - localeBundles: [createFluentBundle()], - localStorage: createInMemoryStorage(), - sessionStorage: createInMemoryStorage(), - rest: new RestClient("http://localhost/api"), - postMessage: new PostMessageService(), - }; - - testRenderer = TestRenderer.create( - - - , - { createNodeMock } - ); + })); }); it("renders comment stream", async () => {