diff --git a/.babelrc b/.babelrc
index 7daf141..980286a 100644
--- a/.babelrc
+++ b/.babelrc
@@ -9,7 +9,11 @@
],
"env": {
"development": {
- "plugins": ["istanbul"]
+ "plugins": [
+ ["istanbul", {
+ "coverageGlobalScopeFunc": false
+ }]
+ ]
}
}
}
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index 871ce5f..054f329 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -22,7 +22,8 @@
"react",
"jsx-a11y",
"@babel",
- "cypress"
+ "cypress",
+ "unused-imports"
],
"rules": {
//Possible errors
@@ -99,6 +100,7 @@
"enforceForJSX": true
}
],
+ "unused-imports/no-unused-imports": "error",
"no-unused-vars": ["error", {
"args": "none"
}],
@@ -108,7 +110,6 @@
"no-void": "error",
"no-warning-comments": "warn",
"prefer-named-capture-group": "error",
- "prefer-promise-reject-errors": "error",
"prefer-regex-literals": ["error", {"disallowRedundantWrapping": true}],
"radix": ["error", "as-needed"],
"require-unicode-regexp": "error",
diff --git a/.gitignore b/.gitignore
index badc771..d68c4b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,14 @@
/.idea/shelf
# Datasource local storage ignored files
/.idea/dataSources/
+/.idea/dataSources.xml
/.idea/dataSources.local.xml
# Editor-based HTTP Client requests
/.idea/httpRequests/
+# Phpstorm files for php
+/.idea/php.xml
+# Plugins
+/.idea/GitLink.xml
dist
lib
@@ -19,3 +24,5 @@ ssl
# Testing & Code Coverage
/.nyc_output/
/coverage/
+/cypress/videos
+/cypress.local.json
diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml
deleted file mode 100644
index b1230ce..0000000
--- a/.idea/dataSources.local.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 28a804d..d7c5271 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,4 +3,9 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
deleted file mode 100644
index 30b4799..0000000
--- a/.idea/php.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Start.xml b/.idea/runConfigurations/Start.xml
deleted file mode 100644
index 0b810fb..0000000
--- a/.idea/runConfigurations/Start.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/stylesheetLinters/stylelint.xml b/.idea/stylesheetLinters/stylelint.xml
deleted file mode 100644
index 551e02b..0000000
--- a/.idea/stylesheetLinters/stylelint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.postcssrc.json b/.postcssrc.json
index 7c824d7..3ed19bd 100644
--- a/.postcssrc.json
+++ b/.postcssrc.json
@@ -1,7 +1,7 @@
{
"plugins": {
"postcss-modules": {
- "generateScopedName": "_parleyMessenger__[local]",
+ "generateScopedName": "[local]__[contenthash]",
"globalModulePaths": [
"index.html"
]
diff --git a/cypress.json b/cypress.json
index 0967ef4..307dc04 100644
--- a/cypress.json
+++ b/cypress.json
@@ -1 +1,3 @@
-{}
+{
+ "baseUrl": "https://chat-dev.parley.nu:8181"
+}
diff --git a/cypress/.eslintrc.json b/cypress/.eslintrc.json
index d27b8f0..d0d386a 100644
--- a/cypress/.eslintrc.json
+++ b/cypress/.eslintrc.json
@@ -1,10 +1,14 @@
{
"extends": "../.eslintrc.json",
+ "plugins": [
+ "no-only-tests"
+ ],
"rules": {
"no-console": "off",
"no-magic-numbers": "off",
"arrow-body-style": "off",
- "max-nested-callbacks": "off"
+ "max-nested-callbacks": "off",
+ "no-only-tests/no-only-tests": "error"
},
"ignorePatterns": ["plugins/index.js", "support/index.js"]
}
\ No newline at end of file
diff --git a/cypress/fixtures/getMessagesResponse.json b/cypress/fixtures/getMessagesResponse.json
new file mode 100644
index 0000000..d25344d
--- /dev/null
+++ b/cypress/fixtures/getMessagesResponse.json
@@ -0,0 +1,39 @@
+{
+ "data": [
+ {
+ "id": 10737,
+ "time": 1536739259,
+ "message": "hi!",
+ "image": null,
+ "typeId": 2,
+ "agent": {
+ "id": 2,
+ "name": "Gerben",
+ "avatar": "https://beta.tracebuzz.com/images/avatars/1912991618/6033.jpg"
+ }
+ },
+ {
+ "id": 10736,
+ "time": 1536739157,
+ "message": "Hello, i have a question",
+ "image": null,
+ "typeId": 1,
+ "agent": null
+ }
+ ],
+ "paging": {
+ "before": "",
+ "after": "/messages/after:10737"
+ },
+ "notifications": [],
+ "status": "SUCCESS",
+ "metadata": {
+ "values": {
+ "url": "messages"
+ },
+ "method": "get",
+ "duration": 0.01
+ },
+ "stickyMessage": "Sorry we are closed right know. We will be open next day from 09:00 - 17:55",
+ "welcomeMessage": "Welcome to our support chat, you can expect a response in ~1 minute."
+}
\ No newline at end of file
diff --git a/cypress/integration/api-class_spec.js b/cypress/integration/api-class_spec.js
index 438b72d..389ccca 100644
--- a/cypress/integration/api-class_spec.js
+++ b/cypress/integration/api-class_spec.js
@@ -302,7 +302,7 @@ describe("Api class", () => {
.to.throw(`Expected string \`version\` to match \`${DeviceVersionRegex}\`, got \`${wrongFormat}\``);
});
- it("should throw an error when using something other than a String as referer", () => {
+ it("should throw an error when using something other than a String as referrer", () => {
filterPrimitives([
"string",
"undefined",
@@ -316,7 +316,26 @@ describe("Api class", () => {
config.version,
set.value,
))
- .to.throw(`Expected \`referer\` to be of type \`string\` but received type \`${set.type}\``);
+ .to.throw(`Expected \`referrer\` to be of type \`string\` but received type \`${set.type}\``);
+ });
+ });
+
+ it("should throw an error when using something other than a String as authorization", () => {
+ filterPrimitives([
+ "string",
+ "undefined", // Don't test for undefined, because authorization is optional and if we give undefined it will test for other params next
+ ]).forEach((set) => {
+ expect(() => config.api.subscribeDevice(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ config.version,
+ undefined,
+ set.value,
+ ))
+ .to.throw(`Expected \`authorization\` to be of type \`string\` but received type \`${set.type}\``);
});
});
@@ -383,7 +402,7 @@ describe("Api class", () => {
});
});
- it("should throw an error when using something other than a String as referer", () => {
+ it("should throw an error when using something other than a String as referrer", () => {
filterPrimitives([
"string",
"undefined",
@@ -392,7 +411,7 @@ describe("Api class", () => {
config.message,
set.value,
))
- .to.throw(`Expected \`referer\` to be of type \`string\` but received type \`${set.type}\``);
+ .to.throw(`Expected \`referrer\` to be of type \`string\` but received type \`${set.type}\``);
});
});
diff --git a/cypress/integration/ui_spec.js b/cypress/integration/ui_spec.js
new file mode 100644
index 0000000..c59387b
--- /dev/null
+++ b/cypress/integration/ui_spec.js
@@ -0,0 +1,750 @@
+import {InterfaceTexts} from "../../src/UI/Scripts/Context";
+
+function clickOnLauncher() {
+ return cy.get("@app")
+ .find("[class^=launcher__]")
+ .find("button")
+ .should("be.visible")
+ .click();
+}
+
+function sendMessage(testMessage) {
+ return cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=footer__]")
+ .should("be.visible")
+ .find("[class^=text__]")
+ .should("be.visible")
+ .find("textarea")
+ .should("have.focus")
+ .type(`${testMessage}{enter}`);
+}
+
+function findMessage(testMessage) {
+ return cy.get("@app")
+ .find("[class^=wrapper__]")
+ .should("be.visible")
+ .find("[class^=body__]")
+ .should("be.visible")
+ .contains(testMessage)
+ .should("be.visible");
+}
+
+describe("UI", () => {
+ describe("sending messages", () => {
+ beforeEach(() => {
+ cy.visit("/", {
+ onLoad: (window) => {
+ window.initParleyMessenger();
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+ });
+
+ it("should send a new message with the new message showing up in the conversation", () => {
+ const testMessage = `Test message ${Date.now()}`;
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+ findMessage(testMessage);
+ });
+
+ it("should show a generic error when the API returns `status = \"ERROR\"`, but without an error", () => {
+ const testMessage = `Test message ${Date.now()}`;
+
+ cy.intercept("POST", "*/**/messages", {
+ statusCode: 400,
+ body: {status: "ERROR"},
+ });
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+
+ // Validate that api error is visible
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("be.visible")
+ .should("have.text", "Something went wrong, please try again later");
+ });
+
+ it("should show the `serviceUnreachableNotification` error when the fetch request fails", () => {
+ const testMessage = `Test message ${Date.now()}`;
+
+ cy.intercept("POST", "*/**/messages", {forceNetworkError: true});
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+
+ // Validate that api error is visible
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("be.visible")
+ .should("have.text", "The service is unreachable at the moment, please try again later");
+ });
+
+ it("should show the `messageSendFailed` error when sending a message fails", () => {
+ const testMessage = `Test message ${Date.now()}`;
+
+ cy.intercept("POST", "*/**/messages", {
+ statusCode: 400,
+ body: {
+ status: "ERROR",
+ notifications: [
+ {
+ type: "error",
+ message: "Some specific error",
+ },
+ ],
+ },
+ });
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+
+ // Validate that api error is visible
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("be.visible")
+ .should("have.text", "Something went wrong while sending your message, please try again later");
+ });
+
+ it("should hide the error when clicking the close error button", () => {
+ const testMessage = `Test message ${Date.now()}`;
+
+ cy.intercept("POST", "*/**/messages", {
+ statusCode: 400,
+ body: {
+ status: "ERROR",
+ notifications: [
+ {
+ type: "error",
+ message: "Some specific error",
+ },
+ ],
+ },
+ });
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+
+ // Validate that api error is visible
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("be.visible")
+ .should("have.text", "Something went wrong while sending your message, please try again later");
+
+ cy.intercept("POST", "*/**/messages"); // Remove handler
+
+ // Click the error close button
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("be.visible")
+ .find("[class^=closeButton__]")
+ .should("be.visible")
+ .click();
+
+ // Validate that the error disappeared
+ cy.get("@app")
+ .find("[class^=chat__]")
+ .should("be.visible")
+ .find("[class^=error__]")
+ .should("not.exist");
+ });
+ });
+
+ describe("parley config settings", () => {
+ describe("runOptions", () => {
+ describe("interfaceTexts", () => {
+ describe("desc", () => {
+ it("should change the title text", () => {
+ const parleyConfig = {runOptions: {interfaceTexts: {desc: "This is the title bar"}}};
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+
+ cy.get("@app")
+ .find("[class^=title__]")
+ .should("have.text", parleyConfig.runOptions.interfaceTexts.desc);
+
+ // Test if it changes during runtime
+ const newTitle = "This is the title bar #2";
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.runOptions.interfaceTexts.desc = newTitle;
+ });
+
+ cy.get("@app")
+ .find("[class^=title__]")
+ .should("have.text", newTitle);
+ });
+ });
+ describe("infoText", () => {
+ it("should change the welcome message", () => {
+ const parleyConfig = {runOptions: {interfaceTexts: {infoText: "This is the info text"}}};
+
+ // To test this we need to ignore the API's `welcomeMessage`
+ cy.fixture("getMessagesResponse.json").then((json) => {
+ const _json = {
+ ...json,
+ welcomeMessage: null,
+ };
+ cy.intercept("GET", "*/**/messages", _json);
+ });
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+
+ cy.get("@app")
+ .find("[class*=announcement__]")
+ .first()
+ .should("have.text", parleyConfig.runOptions.interfaceTexts.infoText);
+
+ // Test if it changes during runtime
+ const newInfoText = "This is the info text #2";
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.runOptions.interfaceTexts.infoText = newInfoText;
+ });
+
+ cy.get("@app")
+ .find("[class*=announcement__]")
+ .first()
+ .should("have.text", newInfoText);
+ });
+ it("should get overridden by API's welcomeMessage", () => {
+ const parleyConfig = {runOptions: {interfaceTexts: {infoText: "This is the info text"}}};
+ const welcomeMessage = "This is the API's welcome message";
+
+ // To test this we need to change the API's `welcomeMessage`
+ cy.fixture("getMessagesResponse.json").then((json) => {
+ const _json = {
+ ...json,
+ welcomeMessage,
+ };
+ cy.intercept("GET", "*/**/messages", _json);
+ });
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+
+ cy.get("@app")
+ .find("[class*=announcement__]")
+ .first()
+ .should("have.text", welcomeMessage);
+ });
+ });
+ describe("placeholderMessenger", () => {
+ it("should change the input's placeholder text", () => {
+ const parleyConfig = {runOptions: {interfaceTexts: {placeholderMessenger: "This is the placeholder"}}};
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+
+ cy.get("@app")
+ .find("[class^=text__]")
+ .find("textarea")
+ .should("have.attr", "placeholder", parleyConfig.runOptions.interfaceTexts.placeholderMessenger);
+
+ // Test if it changes during runtime
+ const newPlaceholder = "This is the placeholder #2";
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.runOptions.interfaceTexts.placeholderMessenger = newPlaceholder;
+ });
+
+ cy.get("@app")
+ .find("[class^=text__]")
+ .find("textarea")
+ .should("have.attr", "placeholder", newPlaceholder);
+ });
+ });
+ });
+ describe("country", () => {
+ it("should change the language of interface texts", () => {
+ const parleyConfig = {
+ runOptions: {
+ country: "en",
+ interfaceTexts: {desc: "Messenger - EN"},
+ },
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (window) => {
+ // eslint-disable-next-line no-param-reassign
+ window.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+
+ cy.get("@app")
+ .find("[class^=text__]")
+ .find("textarea")
+ .should("have.attr", "placeholder", InterfaceTexts.english.inputPlaceholder);
+
+ // Test if it changes during runtime
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.runOptions.country = "nl";
+ });
+
+ cy.get("@app")
+ .find("[class^=text__]")
+ .find("textarea")
+ .should("have.attr", "placeholder", InterfaceTexts.dutch.inputPlaceholder);
+
+ // Extra test to validate that custom interface texts (desc we set above)
+ // have not been overwritten by the new language's defaults
+ cy.get("@app")
+ .find("[class^=title__]")
+ .should("have.text", parleyConfig.runOptions.interfaceTexts.desc);
+ });
+ });
+ });
+ describe("roomNumber", () => {
+ it("should register a new device when switching accounts", () => {
+ const parleyConfig = {roomNumber: "0W4qcE5aXoKq9OzvHxj2"};
+ const testMessage = `test message before switching room numbers${Date.now()}`;
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+ findMessage(testMessage); // Wait until the server received the new message
+
+ // Test if it changes during runtime
+ const newAccountIdentification = "1234";
+ cy.intercept("POST", "*/**/devices", (req) => {
+ expect(req.headers)
+ .to.have.deep.property("x-iris-identification");
+ expect(req.headers["x-iris-identification"])
+ .to.match(new RegExp(`^${newAccountIdentification}:`, "u"));
+ }).as("createDevice");
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.roomNumber = newAccountIdentification;
+ });
+
+ cy.wait("@createDevice");
+ });
+ });
+ describe("authHeader", () => {
+ it("should re-register the device when it changes", () => {
+ const parleyConfig = {roomNumber: "0W4qcE5aXoKq9OzvHxj2"};
+ const testMessage = `test message before switching auth header ${Date.now()}`;
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+ findMessage(testMessage); // Wait until the server received the new message
+
+ // Test if it changes during runtime
+ const newAuthHeader = "1234";
+ cy.intercept("POST", "*/**/devices", (req) => {
+ expect(req.headers)
+ .to.have.deep.property("authorization", newAuthHeader);
+ }).as("createDevice");
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.authHeader = newAuthHeader;
+ });
+
+ cy.wait("@createDevice");
+ });
+ });
+ describe("userAdditionalInformation", () => {
+ it("should re-register the device when it changes", () => {
+ const parleyConfig = {
+ roomNumber: "0W4qcE5aXoKq9OzvHxj2",
+ userAdditionalInformation: {"some-key": "some-value"},
+ };
+ const testMessage = `test message before switching userAdditionalInformation ${Date.now()}`;
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ clickOnLauncher();
+ sendMessage(testMessage);
+ findMessage(testMessage); // Wait until the server received the new message
+
+ // Test if it changes during runtime
+ const newUserAdditionalInformation = {
+ "some-key": "some-value",
+ "some-layer": {"some-key-in-layer": "some-value-in-layer"},
+ };
+
+ cy.intercept("POST", "*/**/devices", (req) => {
+ expect(JSON.parse(req.body))
+ .to.have.deep.property("userAdditionalInformation", newUserAdditionalInformation);
+ }).as("createDevice");
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.userAdditionalInformation["some-layer"] = newUserAdditionalInformation["some-layer"];
+ });
+
+ cy.wait("@createDevice");
+ });
+ });
+ describe("weekdays", () => {
+ describe("format [day, start, end]", () => {
+ it("should show we are offline/online outside/inside working hours", () => {
+ const parleyConfig = {
+ weekdays: [ // closed every day
+ ["Monday"],
+ ["Tuesday"],
+ ["Wednesday"],
+ ["Thursday"],
+ ["Friday"],
+ ["Saturday"],
+ ["Sunday"],
+ ],
+ interface: {hideChatAfterBusinessHours: true},
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ // Launcher is not rendered because we are offline
+ // and outside working hours
+ cy.get("@app")
+ .get("[class^=launcher__]")
+ .should("not.exist");
+
+ // Test if it changes during runtime
+ const newWeekdays = [
+ [
+ "Monday", 0.00, 23.59,
+ ],
+ [
+ "Tuesday", 0.00, 23.59,
+ ],
+ [
+ "Wednesday", 0.00, 23.59,
+ ],
+ [
+ "Thursday", 0.00, 23.59,
+ ],
+ [
+ "Friday", 0.00, 23.59,
+ ],
+ [
+ "Saturday", 0.00, 23.59,
+ ],
+ [
+ "Sunday", 0.00, 23.59,
+ ],
+ ];
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.weekdays = newWeekdays;
+ });
+
+ // Launcher should appear again because we
+ // are inside working hours
+ clickOnLauncher();
+ });
+ });
+ describe("format [day, start, end, true]", () => {
+ it("should show we are offline/online outside/inside working hours", () => {
+ const parleyConfig = {
+ weekdays: [ // closed every day
+ ["Monday"],
+ ["Tuesday"],
+ ["Wednesday"],
+ ["Thursday"],
+ ["Friday"],
+ ["Saturday"],
+ ["Sunday"],
+ ],
+ interface: {hideChatAfterBusinessHours: true},
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ // Launcher is not rendered because we are offline
+ // and outside working hours
+ cy.get("@app")
+ .get("[class^=launcher__]")
+ .should("not.exist");
+
+ // Test if it changes during runtime
+ const newWeekdays = [
+ [
+ "Monday", 0.00, 23.59, true,
+ ],
+ [
+ "Tuesday", 0.00, 23.59, true,
+ ],
+ [
+ "Wednesday", 0.00, 23.59, true,
+ ],
+ [
+ "Thursday", 0.00, 23.59, true,
+ ],
+ [
+ "Friday", 0.00, 23.59, true,
+ ],
+ [
+ "Saturday", 0.00, 23.59, true,
+ ],
+ [
+ "Sunday", 0.00, 23.59, true,
+ ],
+ ];
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.weekdays = newWeekdays;
+ });
+
+ // Launcher should appear again because we
+ // are inside working hours
+ clickOnLauncher();
+ });
+ });
+ describe("format [day, start, end, false]", () => {
+ it("should show we are offline/online outside/inside working hours", () => {
+ const parleyConfig = {
+ weekdays: [ // closed every day
+ ["Monday"],
+ ["Tuesday"],
+ ["Wednesday"],
+ ["Thursday"],
+ ["Friday"],
+ ["Saturday"],
+ ["Sunday"],
+ ],
+ interface: {hideChatAfterBusinessHours: true},
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ // Launcher is not rendered because we are offline
+ // and outside working hours
+ cy.get("@app")
+ .get("[class^=launcher__]")
+ .should("not.exist");
+
+ // Test if it changes during runtime
+ const newWeekdays = [
+ [
+ "Monday", 0.00, 0.00, false,
+ ],
+ [
+ "Tuesday", 0.00, 0.00, false,
+ ],
+ [
+ "Wednesday", 0.00, 0.00, false,
+ ],
+ [
+ "Thursday", 0.00, 0.00, false,
+ ],
+ [
+ "Friday", 0.00, 0.00, false,
+ ],
+ [
+ "Saturday", 0.00, 0.00, false,
+ ],
+ [
+ "Sunday", 0.00, 0.00, false,
+ ],
+ ];
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.weekdays = newWeekdays;
+ });
+
+ // Launcher should appear again because we
+ // are inside working hours
+ clickOnLauncher();
+ });
+ });
+ describe("format [start timestamp, end timestamp]", () => {
+ it("should show we are offline/online outside/inside working hours", () => {
+ const parleyConfig = {
+ weekdays: [
+ [
+ 946681200, 946684800,
+ ], // 2000-01-01 00:00:00 - 2000-01-01 01:00:00
+ ],
+ interface: {hideChatAfterBusinessHours: true},
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ // Launcher is not rendered because we are offline
+ // and outside working hours
+ cy.get("@app")
+ .get("[class^=launcher__]")
+ .should("not.exist");
+
+ // Test if it changes during runtime
+ const startDate = new Date();
+ startDate.setUTCHours(startDate.getUTCHours() - 1);
+ const endDate = new Date();
+ endDate.setUTCHours(endDate.getUTCHours() + 1);
+ const newWeekdays = [
+ [
+ startDate.getTime() / 1000, endDate.getTime() / 1000,
+ ],
+ ];
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.weekdays = newWeekdays;
+ });
+
+ // Launcher should appear again because we
+ // are inside working hours
+ clickOnLauncher();
+ });
+ });
+ describe("format [start timestamp, end timestamp, bool]", () => {
+ it("should show we are offline/online outside/inside working hours", () => {
+ const parleyConfig = {
+ weekdays: [
+ [
+ 946681200, 946684800,
+ ], // 2000-01-01 00:00:00 - 2000-01-01 01:00:00
+ ],
+ interface: {hideChatAfterBusinessHours: true},
+ };
+
+ cy.visit("/", {
+ onBeforeLoad: (win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings = parleyConfig;
+ },
+ });
+
+ cy.get("[id=app]").as("app");
+
+ // Launcher is not rendered because we are offline
+ // and outside working hours
+ cy.get("@app")
+ .get("[class^=launcher__]")
+ .should("not.exist");
+
+ // Test if it changes during runtime
+ const startDate = new Date();
+ startDate.setUTCHours(startDate.getUTCHours() - 1);
+ const endDate = new Date();
+ endDate.setUTCHours(endDate.getUTCHours() + 1);
+ const newWeekdays = [
+ [
+ startDate.getTime() / 1000, endDate.getTime() / 1000, true,
+ ],
+ ];
+
+ cy.window().then((win) => {
+ // eslint-disable-next-line no-param-reassign
+ win.parleySettings.weekdays = newWeekdays;
+ });
+
+ // Launcher should appear again because we
+ // are inside working hours
+ clickOnLauncher();
+ });
+ });
+ });
+ });
+});
diff --git a/cypress/integration/working-hours_spec.js b/cypress/integration/working-hours_spec.js
new file mode 100644
index 0000000..c0256d3
--- /dev/null
+++ b/cypress/integration/working-hours_spec.js
@@ -0,0 +1,427 @@
+import {areWeOnline} from "../../src/UI/Scripts/WorkingHours";
+
+const weekdays = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+];
+
+/**
+ * Creates an array with the formatted start and end time.
+ * Format [08.00, 17.00] ([startTime, endTime])
+ *
+ * @param {Date} startDate
+ * @param {Date} endDate
+ * @return {number[]}
+ */
+function createFormattedWeekday(startDate, endDate) {
+ let startTimeFormatted = `${startDate.getHours()}.${startDate.getMinutes()}`;
+ let endTimeFormatted = `${endDate.getHours()}.${endDate.getUTCMinutes()}`;
+ if(startDate.getMinutes() < 10)
+ startTimeFormatted = `${startDate.getHours()}.0${startDate.getMinutes()}`;
+
+ if(endDate.getMinutes() < 10)
+ endTimeFormatted = `${endDate.getHours()}.0${endDate.getMinutes()}`;
+
+
+ return [
+ parseFloat(startTimeFormatted), parseFloat(endTimeFormatted),
+ ];
+}
+
+/**
+ * Returns the time x minutes into the future
+ * @param minutes
+ * @return {Date}
+ */
+function getTimeInFuture(minutes) {
+ const date = new Date();
+ date.setMinutes(date.getMinutes() + minutes);
+
+ return date;
+}
+
+/**
+ * Returns the time x minutes into the past
+ * @param minutes
+ * @return {Date}
+ */
+function getTimeInPast(minutes) {
+ const date = new Date();
+ date.setMinutes(date.getMinutes() - minutes);
+
+ return date;
+}
+
+/**
+ * Returns the next day for a specific date object
+ * @param date {Date}
+ * @returns {string}
+ */
+function getNextDay(date) {
+ return weekdays[date.getDay() === (weekdays.length - 1) ? 0 : date.getDay() + 1];
+}
+
+describe("Working hours script", () => {
+ describe("areWeOnline()", () => {
+ it("should return true if office hours are not set", () => {
+ expect(areWeOnline()).to.be.equal(true);
+ });
+ describe("format [\"DayOfTheWeek\", startTime, endTime]", () => {
+ it("should return false while outside office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInPast(10);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(10);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return true while inside office hours 00:00 - 23:59", () => {
+ const startDate = new Date();
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ "0.00",
+ "23.59",
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return false when office hours are not numbers", () => {
+ const startDate = new Date();
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ "a.aa",
+ "b.bb",
+ ],
+ ])).to.be.equal(false);
+ });
+ describe("format 2x [\"DayOfTheWeek\", startTime, endTime]", () => {
+ it("should return false while outside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInPast(10);
+ const [
+ startTimeFormatted1, endTimeFormatted1,
+ ] = createFormattedWeekday(startDate1, endDate1);
+
+ const startDate2 = getTimeInPast(60);
+ const endDate2 = getTimeInPast(10);
+ const [
+ startTimeFormatted2, endTimeFormatted2,
+ ] = createFormattedWeekday(startDate2, endDate2);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate1.getDay()],
+ startTimeFormatted1,
+ endTimeFormatted1,
+ ],
+ [
+ getNextDay(startDate2),
+ startTimeFormatted2,
+ endTimeFormatted2,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInFuture(10);
+ const [
+ startTimeFormatted1, endTimeFormatted1,
+ ] = createFormattedWeekday(startDate1, endDate1);
+
+ const startDate2 = getTimeInPast(60);
+ const endDate2 = getTimeInFuture(10);
+ const [
+ startTimeFormatted2, endTimeFormatted2,
+ ] = createFormattedWeekday(startDate2, endDate2);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate1.getDay()],
+ startTimeFormatted1,
+ endTimeFormatted1,
+ ],
+ [
+ getNextDay(startDate2),
+ startTimeFormatted2,
+ endTimeFormatted2,
+ ],
+ ])).to.be.equal(true);
+ });
+ });
+ });
+ describe("format [\"DayOfTheWeek\", startTime, endTime, openOrClosed]", () => {
+ it("should return false while outside OPEN office hours", () => {
+ const startDate = getTimeInFuture(10);
+ const endDate = getTimeInFuture(60);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ true,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside OPEN office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(60);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ true,
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return false while inside CLOSED office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(60);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ false,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while outside CLOSED office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInPast(10);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate, endDate);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ false,
+ ],
+ ])).to.be.equal(true);
+ });
+ });
+ describe("format [startTimeTimestamp, endTimeTimestamp]", () => {
+ it("should return false while outside office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInPast(10);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ ],
+ ])).to.be.equal(true);
+ });
+ describe("format 2x [startTimeTimestamp, endTimeTimestamp]", () => {
+ it("should return false while outside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInPast(10);
+ const startDate2 = getTimeInFuture(10);
+ const endDate2 = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ startDate1.getTime() / 1000,
+ endDate1.getTime() / 1000,
+ ],
+ [
+ startDate2.getTime() / 1000,
+ endDate2.getTime() / 1000,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInPast(10);
+ const startDate2 = getTimeInPast(10);
+ const endDate2 = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ startDate1.getTime() / 1000,
+ endDate1.getTime() / 1000,
+ ],
+ [
+ startDate2.getTime() / 1000,
+ endDate2.getTime() / 1000,
+ ],
+ ])).to.be.equal(true);
+ });
+ });
+ });
+ describe("format [startTimeTimestamp, endTimeTimestamp, openOrClosed]", () => {
+ it("should return false while outside OPEN office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInPast(10);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ true,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside OPEN office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ true,
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return true while outside CLOSED office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInPast(10);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ false,
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return false while inside CLOSED office hours", () => {
+ const startDate = getTimeInPast(60);
+ const endDate = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ startDate.getTime() / 1000,
+ endDate.getTime() / 1000,
+ false,
+ ],
+ ])).to.be.equal(false);
+ });
+ });
+ describe("both formats", () => {
+ it("should return false while outside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInPast(10);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate1, endDate1);
+
+ const startDate2 = getTimeInFuture(10);
+ const endDate2 = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate1.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ ],
+ [
+ startDate2.getTime() / 1000,
+ endDate2.getTime() / 1000,
+ ],
+ ])).to.be.equal(false);
+ });
+ it("should return true while inside office hours", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInPast(10);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate1, endDate1);
+
+ const startDate2 = getTimeInPast(10);
+ const endDate2 = getTimeInFuture(60);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate1.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ ],
+ [
+ startDate2.getTime() / 1000,
+ endDate2.getTime() / 1000,
+ ],
+ ])).to.be.equal(true);
+ });
+ it("should return false while outside office hours (timestamp format), but inside office hours (day format)", () => {
+ const startDate1 = getTimeInPast(60);
+ const endDate1 = getTimeInFuture(60);
+ const [
+ startTimeFormatted, endTimeFormatted,
+ ] = createFormattedWeekday(startDate1, endDate1);
+
+ const startDate2 = getTimeInPast(60);
+ const endDate2 = getTimeInPast(10);
+
+ expect(areWeOnline([
+ [
+ weekdays[startDate1.getDay()],
+ startTimeFormatted,
+ endTimeFormatted,
+ ],
+ [
+ startDate2.getTime() / 1000,
+ endDate2.getTime() / 1000,
+ ],
+ ])).to.be.equal(false);
+ });
+ });
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 5f11360..7733b41 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -19,6 +19,17 @@
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config)
+ // This is needed to have code coverage over our unit tests
+ // We have to use `use-browserify-istanbul` instead of `use-babelrc`
+ // because we use the `coverageGlobalScopeFunc: false` setting
+ // for istanbul in .babelrc to be compliant for our CSP
+ // If we use `use-babelrc` we get an error in Cypress
+ // telling us it can't find `coverage on undefined`
+ on(
+ 'file:preprocessor',
+ require('@cypress/code-coverage/use-browserify-istanbul')
+ )
+
// add other tasks to be registered here
return config
diff --git a/cypress/support/index.js b/cypress/support/index.js
index b44450c..9fb535c 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -19,4 +19,9 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
-import '@cypress/code-coverage/support'
\ No newline at end of file
+import '@cypress/code-coverage/support'
+
+Cypress.on('window:before:load', (win) => {
+ // this lets React DevTools "see" components inside application's iframe
+ win.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__
+})
\ No newline at end of file
diff --git a/index.html b/index.html
index 4557504..00c6550 100644
--- a/index.html
+++ b/index.html
@@ -6,11 +6,9 @@
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
-
Messenger Demo
-
-
-
-
+
+
+
Parley messenger
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-