@@ -1,17 +0,0 @@
|
|||||||
const { defineConfig } = require("cypress");
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
experimentalStudio: true,
|
|
||||||
env: {
|
|
||||||
FIREBASE_USERNAME: "cypress@imex.test",
|
|
||||||
FIREBASE_PASSWORD: "cypress"
|
|
||||||
},
|
|
||||||
e2e: {
|
|
||||||
// We've imported your old cypress plugins here.
|
|
||||||
// You may want to clean this up later by importing these.
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
return require("./cypress/plugins/index.js")(on, config);
|
|
||||||
},
|
|
||||||
baseUrl: "https://localhost:3000"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/// <reference types="Cypress" />
|
|
||||||
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
|
||||||
describe("Renders the General Page", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("/");
|
|
||||||
});
|
|
||||||
it("Renders Correctly", () => {});
|
|
||||||
it("Has the Slogan", () => {
|
|
||||||
cy.findByText("A whole x22new kind of shop management system.").should("exist");
|
|
||||||
/* ==== Generated with Cypress Studio ==== */
|
|
||||||
cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
|
|
||||||
cy.get("#email").clear();
|
|
||||||
cy.get("#email").type("patrick@imex.dev");
|
|
||||||
cy.get("#password").clear();
|
|
||||||
cy.get("#password").type("patrick123{enter}");
|
|
||||||
cy.get(".ant-form > .ant-btn").click();
|
|
||||||
/* ==== End Cypress Studio ==== */
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
// Welcome to Cypress!
|
|
||||||
//
|
|
||||||
// This spec file contains a variety of sample tests
|
|
||||||
// for a todo list app that are designed to demonstrate
|
|
||||||
// the power of writing tests in Cypress.
|
|
||||||
//
|
|
||||||
// To learn more about how Cypress works and
|
|
||||||
// what makes it such an awesome testing tool,
|
|
||||||
// please read our getting started guide:
|
|
||||||
// https://on.cypress.io/introduction-to-cypress
|
|
||||||
|
|
||||||
describe("example to-do app", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Cypress starts out with a blank slate for each test
|
|
||||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
|
||||||
// Since we want to visit the same URL at the start of all our tests,
|
|
||||||
// we include it in our beforeEach function so that it runs before each test
|
|
||||||
cy.visit("https://example.cypress.io/todo");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays two todo items by default", () => {
|
|
||||||
// We use the `cy.get()` command to get all elements that match the selector.
|
|
||||||
// Then, we use `should` to assert that there are two matched items,
|
|
||||||
// which are the two default items.
|
|
||||||
cy.get(".todo-list li").should("have.length", 2);
|
|
||||||
|
|
||||||
// We can go even further and check that the default todos each contain
|
|
||||||
// the correct text. We use the `first` and `last` functions
|
|
||||||
// to get just the first and last matched elements individually,
|
|
||||||
// and then perform an assertion with `should`.
|
|
||||||
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
|
|
||||||
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can add new todo items", () => {
|
|
||||||
// We'll store our item text in a variable so we can reuse it
|
|
||||||
const newItem = "Feed the cat";
|
|
||||||
|
|
||||||
// Let's get the input element and use the `type` command to
|
|
||||||
// input our new list item. After typing the content of our item,
|
|
||||||
// we need to type the enter key as well in order to submit the input.
|
|
||||||
// This input has a data-test attribute so we'll use that to select the
|
|
||||||
// element in accordance with best practices:
|
|
||||||
// https://on.cypress.io/selecting-elements
|
|
||||||
cy.get("[data-test=new-todo]").type(`${newItem}{enter}`);
|
|
||||||
|
|
||||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
|
||||||
// Since it's the newest item, it should exist as the last element in the list.
|
|
||||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
|
||||||
// Since assertions yield the element that was asserted on,
|
|
||||||
// we can chain both of these assertions together into a single statement.
|
|
||||||
cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can check off an item as completed", () => {
|
|
||||||
// In addition to using the `get` command to get an element by selector,
|
|
||||||
// we can also use the `contains` command to get an element by its contents.
|
|
||||||
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
|
||||||
// In order to check the item, we'll find the <input> element for this <label>
|
|
||||||
// by traversing up the dom to the parent element. From there, we can `find`
|
|
||||||
// the child checkbox <input> element and use the `check` command to check it.
|
|
||||||
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
|
|
||||||
|
|
||||||
// Now that we've checked the button, we can go ahead and make sure
|
|
||||||
// that the list element is now marked as completed.
|
|
||||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
|
||||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
|
||||||
// Once we get that element, we can assert that it has the completed class.
|
|
||||||
cy.contains("Pay electric bill").parents("li").should("have.class", "completed");
|
|
||||||
});
|
|
||||||
|
|
||||||
context("with a checked task", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// We'll take the command we used above to check off an element
|
|
||||||
// Since we want to perform multiple tests that start with checking
|
|
||||||
// one element, we put it in the beforeEach hook
|
|
||||||
// so that it runs at the start of every test.
|
|
||||||
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can filter for uncompleted tasks", () => {
|
|
||||||
// We'll click on the "active" button in order to
|
|
||||||
// display only incomplete items
|
|
||||||
cy.contains("Active").click();
|
|
||||||
|
|
||||||
// After filtering, we can assert that there is only the one
|
|
||||||
// incomplete item in the list.
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog");
|
|
||||||
|
|
||||||
// For good measure, let's also assert that the task we checked off
|
|
||||||
// does not exist on the page.
|
|
||||||
cy.contains("Pay electric bill").should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can filter for completed tasks", () => {
|
|
||||||
// We can perform similar steps as the test above to ensure
|
|
||||||
// that only completed tasks are shown
|
|
||||||
cy.contains("Completed").click();
|
|
||||||
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill");
|
|
||||||
|
|
||||||
cy.contains("Walk the dog").should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can delete all completed tasks", () => {
|
|
||||||
// First, let's click the "Clear completed" button
|
|
||||||
// `contains` is actually serving two purposes here.
|
|
||||||
// First, it's ensuring that the button exists within the dom.
|
|
||||||
// This button only appears when at least one task is checked
|
|
||||||
// so this command is implicitly verifying that it does exist.
|
|
||||||
// Second, it selects the button so we can click it.
|
|
||||||
cy.contains("Clear completed").click();
|
|
||||||
|
|
||||||
// Then we can make sure that there is only one element
|
|
||||||
// in the list and our element does not exist
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill");
|
|
||||||
|
|
||||||
// Finally, make sure that the clear button no longer exists.
|
|
||||||
cy.contains("Clear completed").should("not.exist");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Actions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/actions");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
|
||||||
|
|
||||||
it(".type() - type into a DOM element", () => {
|
|
||||||
// https://on.cypress.io/type
|
|
||||||
cy.get(".action-email")
|
|
||||||
.type("fake@email.com")
|
|
||||||
.should("have.value", "fake@email.com")
|
|
||||||
|
|
||||||
// .type() with special character sequences
|
|
||||||
.type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
|
|
||||||
.type("{del}{selectall}{backspace}")
|
|
||||||
|
|
||||||
// .type() with key modifiers
|
|
||||||
.type("{alt}{option}") //these are equivalent
|
|
||||||
.type("{ctrl}{control}") //these are equivalent
|
|
||||||
.type("{meta}{command}{cmd}") //these are equivalent
|
|
||||||
.type("{shift}")
|
|
||||||
|
|
||||||
// Delay each keypress by 0.1 sec
|
|
||||||
.type("slow.typing@email.com", { delay: 100 })
|
|
||||||
.should("have.value", "slow.typing@email.com");
|
|
||||||
|
|
||||||
cy.get(".action-disabled")
|
|
||||||
// Ignore error checking prior to type
|
|
||||||
// like whether the input is visible or disabled
|
|
||||||
.type("disabled error checking", { force: true })
|
|
||||||
.should("have.value", "disabled error checking");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".focus() - focus on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/focus
|
|
||||||
cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".blur() - blur off a DOM element", () => {
|
|
||||||
// https://on.cypress.io/blur
|
|
||||||
cy.get(".action-blur")
|
|
||||||
.type("About to blur")
|
|
||||||
.blur()
|
|
||||||
.should("have.class", "error")
|
|
||||||
.prev()
|
|
||||||
.should("have.attr", "style", "color: red;");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".clear() - clears an input or textarea element", () => {
|
|
||||||
// https://on.cypress.io/clear
|
|
||||||
cy.get(".action-clear")
|
|
||||||
.type("Clear this text")
|
|
||||||
.should("have.value", "Clear this text")
|
|
||||||
.clear()
|
|
||||||
.should("have.value", "");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".submit() - submit a form", () => {
|
|
||||||
// https://on.cypress.io/submit
|
|
||||||
cy.get(".action-form").find('[type="text"]').type("HALFOFF");
|
|
||||||
|
|
||||||
cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".click() - click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/click
|
|
||||||
cy.get(".action-btn").click();
|
|
||||||
|
|
||||||
// You can click on 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// clicking in the center of the element is the default
|
|
||||||
cy.get("#action-canvas").click();
|
|
||||||
|
|
||||||
cy.get("#action-canvas").click("topLeft");
|
|
||||||
cy.get("#action-canvas").click("top");
|
|
||||||
cy.get("#action-canvas").click("topRight");
|
|
||||||
cy.get("#action-canvas").click("left");
|
|
||||||
cy.get("#action-canvas").click("right");
|
|
||||||
cy.get("#action-canvas").click("bottomLeft");
|
|
||||||
cy.get("#action-canvas").click("bottom");
|
|
||||||
cy.get("#action-canvas").click("bottomRight");
|
|
||||||
|
|
||||||
// .click() accepts an x and y coordinate
|
|
||||||
// that controls where the click occurs :)
|
|
||||||
|
|
||||||
cy.get("#action-canvas")
|
|
||||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
|
||||||
.click(170, 75)
|
|
||||||
.click(80, 165)
|
|
||||||
.click(100, 185)
|
|
||||||
.click(125, 190)
|
|
||||||
.click(150, 185)
|
|
||||||
.click(170, 165);
|
|
||||||
|
|
||||||
// click multiple elements by passing multiple: true
|
|
||||||
cy.get(".action-labels>.label").click({ multiple: true });
|
|
||||||
|
|
||||||
// Ignore error checking prior to clicking
|
|
||||||
cy.get(".action-opacity>.btn").click({ force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".dblclick() - double click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/dblclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on double click
|
|
||||||
cy.get(".action-div").dblclick().should("not.be.visible");
|
|
||||||
cy.get(".action-input-hidden").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".rightclick() - right click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/rightclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on right click
|
|
||||||
cy.get(".rightclick-action-div").rightclick().should("not.be.visible");
|
|
||||||
cy.get(".rightclick-action-input-hidden").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".check() - check a checkbox or radio element", () => {
|
|
||||||
// https://on.cypress.io/check
|
|
||||||
|
|
||||||
// By default, .check() will check all
|
|
||||||
// matching checkbox or radio elements in succession, one after another
|
|
||||||
cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
|
|
||||||
|
|
||||||
// .check() accepts a value argument
|
|
||||||
cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
|
|
||||||
|
|
||||||
// .check() accepts an array of values
|
|
||||||
cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
|
|
||||||
|
|
||||||
// Ignore error checking prior to checking
|
|
||||||
cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".uncheck() - uncheck a checkbox element", () => {
|
|
||||||
// https://on.cypress.io/uncheck
|
|
||||||
|
|
||||||
// By default, .uncheck() will uncheck all matching
|
|
||||||
// checkbox elements in succession, one after another
|
|
||||||
cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
|
|
||||||
|
|
||||||
// .uncheck() accepts a value argument
|
|
||||||
cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
|
|
||||||
|
|
||||||
// .uncheck() accepts an array of values
|
|
||||||
cy.get('.action-check [type="checkbox"]')
|
|
||||||
.check(["checkbox1", "checkbox3"])
|
|
||||||
.uncheck(["checkbox1", "checkbox3"])
|
|
||||||
.should("not.be.checked");
|
|
||||||
|
|
||||||
// Ignore error checking prior to unchecking
|
|
||||||
cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".select() - select an option in a <select> element", () => {
|
|
||||||
// https://on.cypress.io/select
|
|
||||||
|
|
||||||
// at first, no option should be selected
|
|
||||||
cy.get(".action-select").should("have.value", "--Select a fruit--");
|
|
||||||
|
|
||||||
// Select option(s) with matching text content
|
|
||||||
cy.get(".action-select").select("apples");
|
|
||||||
// confirm the apples were selected
|
|
||||||
// note that each value starts with "fr-" in our HTML
|
|
||||||
cy.get(".action-select").should("have.value", "fr-apples");
|
|
||||||
|
|
||||||
cy.get(".action-select-multiple")
|
|
||||||
.select(["apples", "oranges", "bananas"])
|
|
||||||
// when getting multiple values, invoke "val" method first
|
|
||||||
.invoke("val")
|
|
||||||
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
|
||||||
|
|
||||||
// Select option(s) with matching value
|
|
||||||
cy.get(".action-select")
|
|
||||||
.select("fr-bananas")
|
|
||||||
// can attach an assertion right away to the element
|
|
||||||
.should("have.value", "fr-bananas");
|
|
||||||
|
|
||||||
cy.get(".action-select-multiple")
|
|
||||||
.select(["fr-apples", "fr-oranges", "fr-bananas"])
|
|
||||||
.invoke("val")
|
|
||||||
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
|
||||||
|
|
||||||
// assert the selected values include oranges
|
|
||||||
cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".scrollIntoView() - scroll an element into view", () => {
|
|
||||||
// https://on.cypress.io/scrollintoview
|
|
||||||
|
|
||||||
// normally all of these buttons are hidden,
|
|
||||||
// because they're not within
|
|
||||||
// the viewable area of their parent
|
|
||||||
// (we need to scroll to see them)
|
|
||||||
cy.get("#scroll-horizontal button").should("not.be.visible");
|
|
||||||
|
|
||||||
// scroll the button into view, as if the user had scrolled
|
|
||||||
cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
|
|
||||||
|
|
||||||
cy.get("#scroll-vertical button").should("not.be.visible");
|
|
||||||
|
|
||||||
// Cypress handles the scroll direction needed
|
|
||||||
cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
|
|
||||||
|
|
||||||
cy.get("#scroll-both button").should("not.be.visible");
|
|
||||||
|
|
||||||
// Cypress knows to scroll to the right and down
|
|
||||||
cy.get("#scroll-both button").scrollIntoView().should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".trigger() - trigger an event on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/trigger
|
|
||||||
|
|
||||||
// To interact with a range input (slider)
|
|
||||||
// we need to set its value & trigger the
|
|
||||||
// event to signal it changed
|
|
||||||
|
|
||||||
// Here, we invoke jQuery's val() method to set
|
|
||||||
// the value and trigger the 'change' event
|
|
||||||
cy.get(".trigger-input-range")
|
|
||||||
.invoke("val", 25)
|
|
||||||
.trigger("change")
|
|
||||||
.get("input[type=range]")
|
|
||||||
.siblings("p")
|
|
||||||
.should("have.text", "25");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.scrollTo() - scroll the window or element to a position", () => {
|
|
||||||
// https://on.cypress.io/scrollto
|
|
||||||
|
|
||||||
// You can scroll to 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// if you chain .scrollTo() off of cy, we will
|
|
||||||
// scroll the entire window
|
|
||||||
cy.scrollTo("bottom");
|
|
||||||
|
|
||||||
cy.get("#scrollable-horizontal").scrollTo("right");
|
|
||||||
|
|
||||||
// or you can scroll to a specific coordinate:
|
|
||||||
// (x axis, y axis) in pixels
|
|
||||||
cy.get("#scrollable-vertical").scrollTo(250, 250);
|
|
||||||
|
|
||||||
// or you can scroll to a specific percentage
|
|
||||||
// of the (width, height) of the element
|
|
||||||
cy.get("#scrollable-both").scrollTo("75%", "25%");
|
|
||||||
|
|
||||||
// control the easing of the scroll (default is 'swing')
|
|
||||||
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
|
|
||||||
|
|
||||||
// control the duration of the scroll (in ms)
|
|
||||||
cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Aliasing", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/aliasing");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".as() - alias a DOM element for later use", () => {
|
|
||||||
// https://on.cypress.io/as
|
|
||||||
|
|
||||||
// Alias a DOM element for use later
|
|
||||||
// We don't have to traverse to the element
|
|
||||||
// later in our code, we reference it with @
|
|
||||||
|
|
||||||
cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
|
|
||||||
|
|
||||||
// when we reference the alias, we place an
|
|
||||||
// @ in front of its name
|
|
||||||
cy.get("@firstBtn").click();
|
|
||||||
|
|
||||||
cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".as() - alias a route for later use", () => {
|
|
||||||
// Alias the route to wait for its response
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("eq", 200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Assertions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/assertions");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Implicit Assertions", () => {
|
|
||||||
it(".should() - make an assertion about the current subject", () => {
|
|
||||||
// https://on.cypress.io/should
|
|
||||||
cy.get(".assertion-table")
|
|
||||||
.find("tbody tr:last")
|
|
||||||
.should("have.class", "success")
|
|
||||||
.find("td")
|
|
||||||
.first()
|
|
||||||
// checking the text of the <td> element in various ways
|
|
||||||
.should("have.text", "Column content")
|
|
||||||
.should("contain", "Column content")
|
|
||||||
.should("have.html", "Column content")
|
|
||||||
// chai-jquery uses "is()" to check if element matches selector
|
|
||||||
.should("match", "td")
|
|
||||||
// to match text content against a regular expression
|
|
||||||
// first need to invoke jQuery method text()
|
|
||||||
// and then match using regular expression
|
|
||||||
.invoke("text")
|
|
||||||
.should("match", /column content/i);
|
|
||||||
|
|
||||||
// a better way to check element's text content against a regular expression
|
|
||||||
// is to use "cy.contains"
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get(".assertion-table")
|
|
||||||
.find("tbody tr:last")
|
|
||||||
// finds first <td> element with text content matching regular expression
|
|
||||||
.contains("td", /column content/i)
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
// for more information about asserting element's text
|
|
||||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".and() - chain multiple assertions together", () => {
|
|
||||||
// https://on.cypress.io/and
|
|
||||||
cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Explicit Assertions", () => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
it("expect - make an assertion about a specified subject", () => {
|
|
||||||
// We can use Chai's BDD style assertions
|
|
||||||
expect(true).to.be.true;
|
|
||||||
const o = { foo: "bar" };
|
|
||||||
|
|
||||||
expect(o).to.equal(o);
|
|
||||||
expect(o).to.deep.equal({ foo: "bar" });
|
|
||||||
// matching text using regular expression
|
|
||||||
expect("FooBar").to.match(/bar$/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("pass your own callback function to should()", () => {
|
|
||||||
// Pass a function to should that can have any number
|
|
||||||
// of explicit assertions within it.
|
|
||||||
// The ".should(cb)" function will be retried
|
|
||||||
// automatically until it passes all your explicit assertions or times out.
|
|
||||||
cy.get(".assertions-p")
|
|
||||||
.find("p")
|
|
||||||
.should(($p) => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
// return an array of texts from all of the p's
|
|
||||||
// @ts-ignore TS6133 unused variable
|
|
||||||
const texts = $p.map((i, el) => Cypress.$(el).text());
|
|
||||||
|
|
||||||
// jquery map returns jquery object
|
|
||||||
// and .get() convert this to simple array
|
|
||||||
const paragraphs = texts.get();
|
|
||||||
|
|
||||||
// array should have length of 3
|
|
||||||
expect(paragraphs, "has 3 paragraphs").to.have.length(3);
|
|
||||||
|
|
||||||
// use second argument to expect(...) to provide clear
|
|
||||||
// message with each assertion
|
|
||||||
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
|
|
||||||
"Some text from first p",
|
|
||||||
"More text from second p",
|
|
||||||
"And even more text from third p"
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds element by class name regex", () => {
|
|
||||||
cy.get(".docs-header")
|
|
||||||
.find("div")
|
|
||||||
// .should(cb) callback function will be retried
|
|
||||||
.should(($div) => {
|
|
||||||
expect($div).to.have.length(1);
|
|
||||||
|
|
||||||
const className = $div[0].className;
|
|
||||||
|
|
||||||
expect(className).to.match(/heading-/);
|
|
||||||
})
|
|
||||||
// .then(cb) callback is not retried,
|
|
||||||
// it either passes or fails
|
|
||||||
.then(($div) => {
|
|
||||||
expect($div, "text content").to.have.text("Introduction");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can throw any error", () => {
|
|
||||||
cy.get(".docs-header")
|
|
||||||
.find("div")
|
|
||||||
.should(($div) => {
|
|
||||||
if ($div.length !== 1) {
|
|
||||||
// you can throw your own errors
|
|
||||||
throw new Error("Did not find 1 element");
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = $div[0].className;
|
|
||||||
|
|
||||||
if (!className.match(/heading-/)) {
|
|
||||||
throw new Error(`Could not find class "heading-" in ${className}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches unknown text between two elements", () => {
|
|
||||||
/**
|
|
||||||
* Text from the first element.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let text;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes passed text,
|
|
||||||
* useful before comparing text with spaces and different capitalization.
|
|
||||||
* @param {string} s Text to normalize
|
|
||||||
*/
|
|
||||||
const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
|
|
||||||
|
|
||||||
cy.get(".two-elements")
|
|
||||||
.find(".first")
|
|
||||||
.then(($first) => {
|
|
||||||
// save text from the first element
|
|
||||||
text = normalizeText($first.text());
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".two-elements")
|
|
||||||
.find(".second")
|
|
||||||
.should(($div) => {
|
|
||||||
// we can massage text before comparing
|
|
||||||
const secondText = normalizeText($div.text());
|
|
||||||
|
|
||||||
expect(secondText, "second text").to.equal(text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("assert - assert shape of an object", () => {
|
|
||||||
const person = {
|
|
||||||
name: "Joe",
|
|
||||||
age: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isObject(person, "value is object");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retries the should callback until assertions pass", () => {
|
|
||||||
cy.get("#random-number").should(($div) => {
|
|
||||||
const n = parseFloat($div.text());
|
|
||||||
|
|
||||||
expect(n).to.be.gte(1).and.be.lte(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Connectors", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/connectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".each() - iterate over an array of elements", () => {
|
|
||||||
// https://on.cypress.io/each
|
|
||||||
cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
|
|
||||||
console.log($el, index, $list);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".its() - get properties on the current subject", () => {
|
|
||||||
// https://on.cypress.io/its
|
|
||||||
cy.get(".connectors-its-ul>li")
|
|
||||||
// calls the 'length' property yielding that value
|
|
||||||
.its("length")
|
|
||||||
.should("be.gt", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".invoke() - invoke a function on the current subject", () => {
|
|
||||||
// our div is hidden in our script.js
|
|
||||||
// $('.connectors-div').hide()
|
|
||||||
|
|
||||||
// https://on.cypress.io/invoke
|
|
||||||
cy.get(".connectors-div")
|
|
||||||
.should("be.hidden")
|
|
||||||
// call the jquery method 'show' on the 'div.container'
|
|
||||||
.invoke("show")
|
|
||||||
.should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".spread() - spread an array as individual args to callback function", () => {
|
|
||||||
// https://on.cypress.io/spread
|
|
||||||
const arr = ["foo", "bar", "baz"];
|
|
||||||
|
|
||||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
|
||||||
expect(foo).to.eq("foo");
|
|
||||||
expect(bar).to.eq("bar");
|
|
||||||
expect(baz).to.eq("baz");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(".then()", () => {
|
|
||||||
it("invokes a callback function with the current subject", () => {
|
|
||||||
// https://on.cypress.io/then
|
|
||||||
cy.get(".connectors-list > li").then(($lis) => {
|
|
||||||
expect($lis, "3 items").to.have.length(3);
|
|
||||||
expect($lis.eq(0), "first item").to.contain("Walk the dog");
|
|
||||||
expect($lis.eq(1), "second item").to.contain("Feed the cat");
|
|
||||||
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the returned value to the next command", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
|
|
||||||
return 2;
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the original subject without return", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
// note that nothing is returned from this callback
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the original unchanged value 1
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the value yielded by the last Cypress command inside", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
// note how we run a Cypress command
|
|
||||||
// the result yielded by this Cypress command
|
|
||||||
// will be passed to the second ".then"
|
|
||||||
cy.wrap(2);
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the value yielded by "cy.wrap(2)"
|
|
||||||
expect(num).to.equal(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Cookies", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Cypress.Cookies.debug(true);
|
|
||||||
|
|
||||||
cy.visit("https://example.cypress.io/commands/cookies");
|
|
||||||
|
|
||||||
// clear cookies again after visiting to remove
|
|
||||||
// any 3rd party cookies picked up such as cloudflare
|
|
||||||
cy.clearCookies();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.getCookie() - get a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/getcookie
|
|
||||||
cy.get("#getCookie .set-a-cookie").click();
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie("token").should("have.property", "value", "123ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.getCookies() - get browser cookies", () => {
|
|
||||||
// https://on.cypress.io/getcookies
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.get("#getCookies .set-a-cookie").click();
|
|
||||||
|
|
||||||
// cy.getCookies() yields an array of cookies
|
|
||||||
cy.getCookies()
|
|
||||||
.should("have.length", 1)
|
|
||||||
.should((cookies) => {
|
|
||||||
// each cookie has these properties
|
|
||||||
expect(cookies[0]).to.have.property("name", "token");
|
|
||||||
expect(cookies[0]).to.have.property("value", "123ABC");
|
|
||||||
expect(cookies[0]).to.have.property("httpOnly", false);
|
|
||||||
expect(cookies[0]).to.have.property("secure", false);
|
|
||||||
expect(cookies[0]).to.have.property("domain");
|
|
||||||
expect(cookies[0]).to.have.property("path");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.setCookie() - set a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/setcookie
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.setCookie("foo", "bar");
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie("foo").should("have.property", "value", "bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clearCookie() - clear a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/clearcookie
|
|
||||||
cy.getCookie("token").should("be.null");
|
|
||||||
|
|
||||||
cy.get("#clearCookie .set-a-cookie").click();
|
|
||||||
|
|
||||||
cy.getCookie("token").should("have.property", "value", "123ABC");
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookie("token").should("be.null");
|
|
||||||
|
|
||||||
cy.getCookie("token").should("be.null");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clearCookies() - clear browser cookies", () => {
|
|
||||||
// https://on.cypress.io/clearcookies
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.get("#clearCookies .set-a-cookie").click();
|
|
||||||
|
|
||||||
cy.getCookies().should("have.length", 1);
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookies();
|
|
||||||
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Cypress.Commands", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
|
|
||||||
it(".add() - create a custom command", () => {
|
|
||||||
Cypress.Commands.add(
|
|
||||||
"console",
|
|
||||||
{
|
|
||||||
prevSubject: true
|
|
||||||
},
|
|
||||||
(subject, method) => {
|
|
||||||
// the previous subject is automatically received
|
|
||||||
// and the commands arguments are shifted
|
|
||||||
|
|
||||||
// allow us to change the console method used
|
|
||||||
method = method || "log";
|
|
||||||
|
|
||||||
// log the subject to the console
|
|
||||||
// @ts-ignore TS7017
|
|
||||||
console[method]("The subject is", subject);
|
|
||||||
|
|
||||||
// whatever we return becomes the new subject
|
|
||||||
// we don't want to change the subject so
|
|
||||||
// we return whatever was passed in
|
|
||||||
return subject;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-ignore TS2339
|
|
||||||
cy.get("button")
|
|
||||||
.console("info")
|
|
||||||
.then(($button) => {
|
|
||||||
// subject is still $button
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.Cookies", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/cookies
|
|
||||||
it(".debug() - enable or disable debugging", () => {
|
|
||||||
Cypress.Cookies.debug(true);
|
|
||||||
|
|
||||||
// Cypress will now log in the console when
|
|
||||||
// cookies are set or cleared
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
cy.clearCookie("fakeCookie");
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
cy.clearCookie("fakeCookie");
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".preserveOnce() - preserve cookies by key", () => {
|
|
||||||
// normally cookies are reset after each test
|
|
||||||
cy.getCookie("fakeCookie").should("not.be.ok");
|
|
||||||
|
|
||||||
// preserving a cookie will not clear it when
|
|
||||||
// the next test starts
|
|
||||||
cy.setCookie("lastCookie", "789XYZ");
|
|
||||||
Cypress.Cookies.preserveOnce("lastCookie");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".defaults() - set defaults for all cookies", () => {
|
|
||||||
// now any cookie with the name 'session_id' will
|
|
||||||
// not be cleared before each new test runs
|
|
||||||
Cypress.Cookies.defaults({
|
|
||||||
preserve: "session_id"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.arch", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get CPU architecture name of underlying OS", () => {
|
|
||||||
// https://on.cypress.io/arch
|
|
||||||
expect(Cypress.arch).to.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.config()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get and set configuration options", () => {
|
|
||||||
// https://on.cypress.io/config
|
|
||||||
let myConfig = Cypress.config();
|
|
||||||
|
|
||||||
expect(myConfig).to.have.property("animationDistanceThreshold", 5);
|
|
||||||
expect(myConfig).to.have.property("baseUrl", null);
|
|
||||||
expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
|
|
||||||
expect(myConfig).to.have.property("requestTimeout", 5000);
|
|
||||||
expect(myConfig).to.have.property("responseTimeout", 30000);
|
|
||||||
expect(myConfig).to.have.property("viewportHeight", 660);
|
|
||||||
expect(myConfig).to.have.property("viewportWidth", 1000);
|
|
||||||
expect(myConfig).to.have.property("pageLoadTimeout", 60000);
|
|
||||||
expect(myConfig).to.have.property("waitForAnimations", true);
|
|
||||||
|
|
||||||
expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
|
|
||||||
|
|
||||||
// this will change the config for the rest of your tests!
|
|
||||||
Cypress.config("pageLoadTimeout", 20000);
|
|
||||||
|
|
||||||
expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
|
|
||||||
|
|
||||||
Cypress.config("pageLoadTimeout", 60000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.dom", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/dom
|
|
||||||
it(".isHidden() - determine if a DOM element is hidden", () => {
|
|
||||||
let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
|
|
||||||
let visibleP = Cypress.$(".dom-p p.visible").get(0);
|
|
||||||
|
|
||||||
// our first paragraph has css class 'hidden'
|
|
||||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
|
|
||||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.env()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// We can set environment variables for highly dynamic values
|
|
||||||
|
|
||||||
// https://on.cypress.io/environment-variables
|
|
||||||
it("Get environment variables", () => {
|
|
||||||
// https://on.cypress.io/env
|
|
||||||
// set multiple environment variables
|
|
||||||
Cypress.env({
|
|
||||||
host: "veronica.dev.local",
|
|
||||||
api_server: "http://localhost:8888/v1/"
|
|
||||||
});
|
|
||||||
|
|
||||||
// get environment variable
|
|
||||||
expect(Cypress.env("host")).to.eq("veronica.dev.local");
|
|
||||||
|
|
||||||
// set environment variable
|
|
||||||
Cypress.env("api_server", "http://localhost:8888/v2/");
|
|
||||||
expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
|
|
||||||
|
|
||||||
// get all environment variable
|
|
||||||
expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
|
|
||||||
expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.log", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Control what is printed to the Command Log", () => {
|
|
||||||
// https://on.cypress.io/cypress-log
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.platform", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get underlying OS name", () => {
|
|
||||||
// https://on.cypress.io/platform
|
|
||||||
expect(Cypress.platform).to.be.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.version", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get current version of Cypress being run", () => {
|
|
||||||
// https://on.cypress.io/version
|
|
||||||
expect(Cypress.version).to.be.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.spec", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get current spec information", () => {
|
|
||||||
// https://on.cypress.io/spec
|
|
||||||
// wrap the object so we can inspect it easily by clicking in the command log
|
|
||||||
cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
/// JSON fixture file can be loaded directly using
|
|
||||||
// the built-in JavaScript bundler
|
|
||||||
// @ts-ignore
|
|
||||||
const requiredExample = require("../../fixtures/example");
|
|
||||||
|
|
||||||
context("Files", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/files");
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// load example.json fixture file and store
|
|
||||||
// in the test context object
|
|
||||||
cy.fixture("example.json").as("example");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.fixture() - load a fixture", () => {
|
|
||||||
// https://on.cypress.io/fixture
|
|
||||||
|
|
||||||
// Instead of writing a response inline you can
|
|
||||||
// use a fixture file's content.
|
|
||||||
|
|
||||||
// when application makes an Ajax request matching "GET **/comments/*"
|
|
||||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
|
||||||
cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".fixture-btn").click();
|
|
||||||
|
|
||||||
cy.wait("@getComment")
|
|
||||||
.its("response.body")
|
|
||||||
.should("have.property", "name")
|
|
||||||
.and("include", "Using fixtures to represent data");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.fixture() or require - load a fixture", function () {
|
|
||||||
// we are inside the "function () { ... }"
|
|
||||||
// callback and can use test context object "this"
|
|
||||||
// "this.example" was loaded in "beforeEach" function callback
|
|
||||||
expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
|
|
||||||
|
|
||||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
|
||||||
cy.wrap(this.example).should("deep.equal", requiredExample);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.readFile() - read file contents", () => {
|
|
||||||
// https://on.cypress.io/readfile
|
|
||||||
|
|
||||||
// You can read a file and yield its contents
|
|
||||||
// The filePath is relative to your project's root.
|
|
||||||
cy.readFile("cypress.json").then((json) => {
|
|
||||||
expect(json).to.be.an("object");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.writeFile() - write to a file", () => {
|
|
||||||
// https://on.cypress.io/writefile
|
|
||||||
|
|
||||||
// You can write to a file
|
|
||||||
|
|
||||||
// Use a response from a request to automatically
|
|
||||||
// generate a fixture file for use later
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
|
||||||
cy.writeFile("cypress/fixtures/users.json", response.body);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.fixture("users").should((users) => {
|
|
||||||
expect(users[0].name).to.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
// JavaScript arrays and objects are stringified
|
|
||||||
// and formatted into text.
|
|
||||||
cy.writeFile("cypress/fixtures/profile.json", {
|
|
||||||
id: 8739,
|
|
||||||
name: "Jane",
|
|
||||||
email: "jane@example.com"
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.fixture("profile").should((profile) => {
|
|
||||||
expect(profile.name).to.eq("Jane");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Local Storage", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/local-storage");
|
|
||||||
});
|
|
||||||
// Although local storage is automatically cleared
|
|
||||||
// in between tests to maintain a clean state
|
|
||||||
// sometimes we need to clear the local storage manually
|
|
||||||
|
|
||||||
it("cy.clearLocalStorage() - clear all data in local storage", () => {
|
|
||||||
// https://on.cypress.io/clearlocalstorage
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// clearLocalStorage() yields the localStorage object
|
|
||||||
cy.clearLocalStorage().should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.be.null;
|
|
||||||
expect(ls.getItem("prop3")).to.be.null;
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear key matching string in Local Storage
|
|
||||||
cy.clearLocalStorage("prop1").should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(ls.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear keys matching regex in Local Storage
|
|
||||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.be.null;
|
|
||||||
expect(ls.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Location", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/location");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.hash() - get the current URL hash", () => {
|
|
||||||
// https://on.cypress.io/hash
|
|
||||||
cy.hash().should("be.empty");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.location() - get window.location", () => {
|
|
||||||
// https://on.cypress.io/location
|
|
||||||
cy.location().should((location) => {
|
|
||||||
expect(location.hash).to.be.empty;
|
|
||||||
expect(location.href).to.eq("https://example.cypress.io/commands/location");
|
|
||||||
expect(location.host).to.eq("example.cypress.io");
|
|
||||||
expect(location.hostname).to.eq("example.cypress.io");
|
|
||||||
expect(location.origin).to.eq("https://example.cypress.io");
|
|
||||||
expect(location.pathname).to.eq("/commands/location");
|
|
||||||
expect(location.port).to.eq("");
|
|
||||||
expect(location.protocol).to.eq("https:");
|
|
||||||
expect(location.search).to.be.empty;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.url() - get the current URL", () => {
|
|
||||||
// https://on.cypress.io/url
|
|
||||||
cy.url().should("eq", "https://example.cypress.io/commands/location");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Misc", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/misc");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".end() - end the command chain", () => {
|
|
||||||
// https://on.cypress.io/end
|
|
||||||
|
|
||||||
// cy.end is useful when you want to end a chain of commands
|
|
||||||
// and force Cypress to re-query from the root element
|
|
||||||
cy.get(".misc-table").within(() => {
|
|
||||||
// ends the current chain and yields null
|
|
||||||
cy.contains("Cheryl").click().end();
|
|
||||||
|
|
||||||
// queries the entire table again
|
|
||||||
cy.contains("Charles").click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.exec() - execute a system command", () => {
|
|
||||||
// execute a system command.
|
|
||||||
// so you can take actions necessary for
|
|
||||||
// your test outside the scope of Cypress.
|
|
||||||
// https://on.cypress.io/exec
|
|
||||||
|
|
||||||
// we can use Cypress.platform string to
|
|
||||||
// select appropriate command
|
|
||||||
// https://on.cypress/io/platform
|
|
||||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
|
|
||||||
|
|
||||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
|
||||||
// https://github.com/cypress-io/cypress/issues/5169
|
|
||||||
// so skip some of the tests by passing flag "--env circle=true"
|
|
||||||
const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
|
|
||||||
|
|
||||||
if (isCircleOnWindows) {
|
|
||||||
cy.log("Skipping test on CircleCI");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cy.exec problem on Shippable CI
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6718
|
|
||||||
const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
|
|
||||||
|
|
||||||
if (isShippable) {
|
|
||||||
cy.log("Skipping test on ShippableCI");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
|
|
||||||
|
|
||||||
if (Cypress.platform === "win32") {
|
|
||||||
cy.exec("print cypress.json").its("stderr").should("be.empty");
|
|
||||||
} else {
|
|
||||||
cy.exec("cat cypress.json").its("stderr").should("be.empty");
|
|
||||||
|
|
||||||
cy.exec("pwd").its("code").should("eq", 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.focused() - get the DOM element that has focus", () => {
|
|
||||||
// https://on.cypress.io/focused
|
|
||||||
cy.get(".misc-form").find("#name").click();
|
|
||||||
cy.focused().should("have.id", "name");
|
|
||||||
|
|
||||||
cy.get(".misc-form").find("#description").click();
|
|
||||||
cy.focused().should("have.id", "description");
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.Screenshot", function () {
|
|
||||||
it("cy.screenshot() - take a screenshot", () => {
|
|
||||||
// https://on.cypress.io/screenshot
|
|
||||||
cy.screenshot("my-image");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Screenshot.defaults() - change default config of screenshots", function () {
|
|
||||||
Cypress.Screenshot.defaults({
|
|
||||||
blackout: [".foo"],
|
|
||||||
capture: "viewport",
|
|
||||||
clip: { x: 0, y: 0, width: 200, height: 200 },
|
|
||||||
scale: false,
|
|
||||||
disableTimersAndAnimations: true,
|
|
||||||
screenshotOnRunFailure: true,
|
|
||||||
onBeforeScreenshot() {},
|
|
||||||
onAfterScreenshot() {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.wrap() - wrap an object", () => {
|
|
||||||
// https://on.cypress.io/wrap
|
|
||||||
cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Navigation", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io");
|
|
||||||
cy.get(".navbar-nav").contains("Commands").click();
|
|
||||||
cy.get(".dropdown-menu").contains("Navigation").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.go() - go back or forward in the browser's history", () => {
|
|
||||||
// https://on.cypress.io/go
|
|
||||||
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
|
|
||||||
cy.go("back");
|
|
||||||
cy.location("pathname").should("not.include", "navigation");
|
|
||||||
|
|
||||||
cy.go("forward");
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
|
|
||||||
// clicking back
|
|
||||||
cy.go(-1);
|
|
||||||
cy.location("pathname").should("not.include", "navigation");
|
|
||||||
|
|
||||||
// clicking forward
|
|
||||||
cy.go(1);
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.reload() - reload the page", () => {
|
|
||||||
// https://on.cypress.io/reload
|
|
||||||
cy.reload();
|
|
||||||
|
|
||||||
// reload the page without using the cache
|
|
||||||
cy.reload(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.visit() - visit a remote url", () => {
|
|
||||||
// https://on.cypress.io/visit
|
|
||||||
|
|
||||||
// Visit any sub-domain of your current domain
|
|
||||||
|
|
||||||
// Pass options to the visit
|
|
||||||
cy.visit("https://example.cypress.io/commands/navigation", {
|
|
||||||
timeout: 50000, // increase total time for the visit to resolve
|
|
||||||
onBeforeLoad(contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === "object").to.be.true;
|
|
||||||
},
|
|
||||||
onLoad(contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === "object").to.be.true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Network Requests", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/network-requests");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manage HTTP requests in your app
|
|
||||||
|
|
||||||
it("cy.request() - make an XHR request", () => {
|
|
||||||
// https://on.cypress.io/request
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
|
|
||||||
expect(response.status).to.eq(200);
|
|
||||||
// the server sometimes gets an extra comment posted from another machine
|
|
||||||
// which gets returned as 1 extra object
|
|
||||||
expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
|
|
||||||
expect(response).to.have.property("headers");
|
|
||||||
expect(response).to.have.property("duration");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - verify response using BDD syntax", () => {
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
expect(response).property("status").to.equal(200);
|
|
||||||
expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
|
|
||||||
expect(response).to.include.keys("headers", "duration");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() with query parameters", () => {
|
|
||||||
// will execute request
|
|
||||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
|
||||||
cy.request({
|
|
||||||
url: "https://jsonplaceholder.cypress.io/comments",
|
|
||||||
qs: {
|
|
||||||
postId: 1,
|
|
||||||
id: 3
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.its("body")
|
|
||||||
.should("be.an", "array")
|
|
||||||
.and("have.length", 1)
|
|
||||||
.its("0") // yields first element of the array
|
|
||||||
.should("contain", {
|
|
||||||
postId: 1,
|
|
||||||
id: 3
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - pass result to the second request", () => {
|
|
||||||
// first, let's find out the userId of the first user we have
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
|
||||||
.its("body") // yields the response object
|
|
||||||
.its("0") // yields the first element of the returned list
|
|
||||||
// the above two commands its('body').its('0')
|
|
||||||
// can be written as its('body.0')
|
|
||||||
// if you do not care about TypeScript checks
|
|
||||||
.then((user) => {
|
|
||||||
expect(user).property("id").to.be.a("number");
|
|
||||||
// make a new post on behalf of the user
|
|
||||||
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
|
||||||
userId: user.id,
|
|
||||||
title: "Cypress Test Runner",
|
|
||||||
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// note that the value here is the returned value of the 2nd request
|
|
||||||
// which is the new post object
|
|
||||||
.then((response) => {
|
|
||||||
expect(response).property("status").to.equal(201); // new entity created
|
|
||||||
expect(response).property("body").to.contain({
|
|
||||||
title: "Cypress Test Runner"
|
|
||||||
});
|
|
||||||
|
|
||||||
// we don't know the exact post id - only that it will be > 100
|
|
||||||
// since JSONPlaceholder has built-in 100 posts
|
|
||||||
expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
|
|
||||||
|
|
||||||
// we don't know the user id here - since it was in above closure
|
|
||||||
// so in this test just confirm that the property is there
|
|
||||||
expect(response.body).property("userId").to.be.a("number");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - save response in the shared test context", () => {
|
|
||||||
// https://on.cypress.io/variables-and-aliases
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
|
||||||
.its("body")
|
|
||||||
.its("0") // yields the first element of the returned list
|
|
||||||
.as("user") // saves the object in the test context
|
|
||||||
.then(function () {
|
|
||||||
// NOTE 👀
|
|
||||||
// By the time this callback runs the "as('user')" command
|
|
||||||
// has saved the user object in the test context.
|
|
||||||
// To access the test context we need to use
|
|
||||||
// the "function () { ... }" callback form,
|
|
||||||
// otherwise "this" points at a wrong or undefined object!
|
|
||||||
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
|
||||||
userId: this.user.id,
|
|
||||||
title: "Cypress Test Runner",
|
|
||||||
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
|
||||||
})
|
|
||||||
.its("body")
|
|
||||||
.as("post"); // save the new post from the response
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
// When this callback runs, both "cy.request" API commands have finished
|
|
||||||
// and the test context has "user" and "post" objects set.
|
|
||||||
// Let's verify them.
|
|
||||||
expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.intercept() - route responses to matching requests", () => {
|
|
||||||
// https://on.cypress.io/intercept
|
|
||||||
|
|
||||||
let message = "whoa, this comment does not exist";
|
|
||||||
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
|
||||||
|
|
||||||
// Listen to POST to comments
|
|
||||||
cy.intercept("POST", "**/comments").as("postComment");
|
|
||||||
|
|
||||||
// we have code that posts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-post").click();
|
|
||||||
cy.wait("@postComment").should(({ request, response }) => {
|
|
||||||
expect(request.body).to.include("email");
|
|
||||||
expect(request.headers).to.have.property("content-type");
|
|
||||||
expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stub a response to PUT comments/ ****
|
|
||||||
cy.intercept(
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
url: "**/comments/*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
statusCode: 404,
|
|
||||||
body: { error: message },
|
|
||||||
headers: { "access-control-allow-origin": "*" },
|
|
||||||
delayMs: 500
|
|
||||||
}
|
|
||||||
).as("putComment");
|
|
||||||
|
|
||||||
// we have code that puts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-put").click();
|
|
||||||
|
|
||||||
cy.wait("@putComment");
|
|
||||||
|
|
||||||
// our 404 statusCode logic in scripts.js executed
|
|
||||||
cy.get(".network-put-comment").should("contain", message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Querying", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/querying");
|
|
||||||
});
|
|
||||||
|
|
||||||
// The most commonly used query is 'cy.get()', you can
|
|
||||||
// think of this like the '$' in jQuery
|
|
||||||
|
|
||||||
it("cy.get() - query DOM elements", () => {
|
|
||||||
// https://on.cypress.io/get
|
|
||||||
|
|
||||||
cy.get("#query-btn").should("contain", "Button");
|
|
||||||
|
|
||||||
cy.get(".query-btn").should("contain", "Button");
|
|
||||||
|
|
||||||
cy.get("#querying .well>button:first").should("contain", "Button");
|
|
||||||
// ↲
|
|
||||||
// Use CSS selectors just like jQuery
|
|
||||||
|
|
||||||
cy.get('[data-test-id="test-example"]').should("have.class", "example");
|
|
||||||
|
|
||||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
|
||||||
// by invoking `.attr()` method
|
|
||||||
cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
|
|
||||||
|
|
||||||
// or you can get element's CSS property
|
|
||||||
cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
|
|
||||||
|
|
||||||
// or use assertions directly during 'cy.get()'
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
cy.get('[data-test-id="test-example"]')
|
|
||||||
.should("have.attr", "data-test-id", "test-example")
|
|
||||||
.and("have.css", "position", "static");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.contains() - query DOM elements with matching content", () => {
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get(".query-list").contains("bananas").should("have.class", "third");
|
|
||||||
|
|
||||||
// we can pass a regexp to `.contains()`
|
|
||||||
cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
|
|
||||||
|
|
||||||
cy.get(".query-list").contains("apples").should("have.class", "first");
|
|
||||||
|
|
||||||
// passing a selector to contains will
|
|
||||||
// yield the selector containing the text
|
|
||||||
cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
|
|
||||||
|
|
||||||
cy.get(".query-button").contains("Save Form").should("have.class", "btn");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".within() - query DOM elements within a specific element", () => {
|
|
||||||
// https://on.cypress.io/within
|
|
||||||
cy.get(".query-form").within(() => {
|
|
||||||
cy.get("input:first").should("have.attr", "placeholder", "Email");
|
|
||||||
cy.get("input:last").should("have.attr", "placeholder", "Password");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.root() - query the root DOM element", () => {
|
|
||||||
// https://on.cypress.io/root
|
|
||||||
|
|
||||||
// By default, root is the document
|
|
||||||
cy.root().should("match", "html");
|
|
||||||
|
|
||||||
cy.get(".query-ul").within(() => {
|
|
||||||
// In this within, the root is now the ul DOM element
|
|
||||||
cy.root().should("have.class", "query-ul");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("best practices - selecting elements", () => {
|
|
||||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
|
||||||
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
|
|
||||||
// Worst - too generic, no context
|
|
||||||
cy.get("button").click();
|
|
||||||
|
|
||||||
// Bad. Coupled to styling. Highly subject to change.
|
|
||||||
cy.get(".btn.btn-large").click();
|
|
||||||
|
|
||||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
|
||||||
cy.get("[name=submission]").click();
|
|
||||||
|
|
||||||
// Better. But still coupled to styling or JS event listeners.
|
|
||||||
cy.get("#main").click();
|
|
||||||
|
|
||||||
// Slightly better. Uses an ID but also ensures the element
|
|
||||||
// has an ARIA role attribute
|
|
||||||
cy.get("#main[role=button]").click();
|
|
||||||
|
|
||||||
// Much better. But still coupled to text content that may change.
|
|
||||||
cy.contains("Submit").click();
|
|
||||||
|
|
||||||
// Best. Insulated from all changes.
|
|
||||||
cy.get("[data-cy=submit]").click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// remove no check once Cypress.sinon is typed
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6720
|
|
||||||
|
|
||||||
context("Spies, Stubs, and Clock", () => {
|
|
||||||
it("cy.spy() - wrap a method in a spy", () => {
|
|
||||||
// https://on.cypress.io/spy
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
foo() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spy = cy.spy(obj, "foo").as("anyArgs");
|
|
||||||
|
|
||||||
obj.foo();
|
|
||||||
|
|
||||||
expect(spy).to.be.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.spy() retries until assertions pass", () => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* Prints the argument passed
|
|
||||||
* @param x {any}
|
|
||||||
*/
|
|
||||||
foo(x) {
|
|
||||||
console.log("obj.foo called with", x);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.spy(obj, "foo").as("foo");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo("first");
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo("second");
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
cy.get("@foo").should("have.been.calledTwice");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.stub() - create a stub and/or replace a function with stub", () => {
|
|
||||||
// https://on.cypress.io/stub
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* prints both arguments to the console
|
|
||||||
* @param a {string}
|
|
||||||
* @param b {string}
|
|
||||||
*/
|
|
||||||
foo(a, b) {
|
|
||||||
console.log("a", a, "b", b);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stub = cy.stub(obj, "foo").as("foo");
|
|
||||||
|
|
||||||
obj.foo("foo", "bar");
|
|
||||||
|
|
||||||
expect(stub).to.be.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clock() - control time in the browser", () => {
|
|
||||||
// https://on.cypress.io/clock
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
|
||||||
|
|
||||||
cy.clock(now);
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
cy.get("#clock-div").click().should("have.text", "1489449600");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.tick() - move time in the browser", () => {
|
|
||||||
// https://on.cypress.io/tick
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
|
||||||
|
|
||||||
cy.clock(now);
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
cy.get("#tick-div").click().should("have.text", "1489449600");
|
|
||||||
|
|
||||||
cy.tick(10000); // 10 seconds passed
|
|
||||||
cy.get("#tick-div").click().should("have.text", "1489449610");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.stub() matches depending on arguments", () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const greeter = {
|
|
||||||
/**
|
|
||||||
* Greets a person
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
greet(name) {
|
|
||||||
return `Hello, ${name}!`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.stub(greeter, "greet")
|
|
||||||
.callThrough() // if you want non-matched calls to call the real method
|
|
||||||
.withArgs(Cypress.sinon.match.string)
|
|
||||||
.returns("Hi")
|
|
||||||
.withArgs(Cypress.sinon.match.number)
|
|
||||||
.throws(new Error("Invalid name"));
|
|
||||||
|
|
||||||
expect(greeter.greet("World")).to.equal("Hi");
|
|
||||||
// @ts-ignore
|
|
||||||
expect(() => greeter.greet(42)).to.throw("Invalid name");
|
|
||||||
expect(greeter.greet).to.have.been.calledTwice;
|
|
||||||
|
|
||||||
// non-matched calls goes the actual method
|
|
||||||
// @ts-ignore
|
|
||||||
expect(greeter.greet()).to.equal("Hello, undefined!");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches call arguments using Sinon matchers", () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const calculator = {
|
|
||||||
/**
|
|
||||||
* returns the sum of two arguments
|
|
||||||
* @param a {number}
|
|
||||||
* @param b {number}
|
|
||||||
*/
|
|
||||||
add(a, b) {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spy = cy.spy(calculator, "add").as("add");
|
|
||||||
|
|
||||||
expect(calculator.add(2, 3)).to.equal(5);
|
|
||||||
|
|
||||||
// if we want to assert the exact values used during the call
|
|
||||||
expect(spy).to.be.calledWith(2, 3);
|
|
||||||
|
|
||||||
// let's confirm "add" method was called with two numbers
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
|
|
||||||
|
|
||||||
// alternatively, provide the value to match
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
|
|
||||||
|
|
||||||
// match any value
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
|
|
||||||
|
|
||||||
// match any value from a list
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given number is event
|
|
||||||
* @param {number} x
|
|
||||||
*/
|
|
||||||
const isEven = (x) => x % 2 === 0;
|
|
||||||
|
|
||||||
// expect the value to pass a custom predicate function
|
|
||||||
// the second argument to "sinon.match(predicate, message)" is
|
|
||||||
// shown if the predicate does not pass and assertion fails
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is larger than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isGreaterThan = (limit) => (x) => x > limit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is less than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isLessThan = (limit) => (x) => x < limit;
|
|
||||||
|
|
||||||
// you can combine several matchers using "and", "or"
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
|
|
||||||
);
|
|
||||||
|
|
||||||
// matchers can be used from BDD assertions
|
|
||||||
cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
|
|
||||||
|
|
||||||
// you can alias matchers for shorter test code
|
|
||||||
const { match: M } = Cypress.sinon;
|
|
||||||
|
|
||||||
cy.get("@add").should("have.been.calledWith", M.number, M(3));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Traversal", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/traversal");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".children() - get child DOM elements", () => {
|
|
||||||
// https://on.cypress.io/children
|
|
||||||
cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".closest() - get closest ancestor DOM element", () => {
|
|
||||||
// https://on.cypress.io/closest
|
|
||||||
cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".eq() - get a DOM element at a specific index", () => {
|
|
||||||
// https://on.cypress.io/eq
|
|
||||||
cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".filter() - get DOM elements that match the selector", () => {
|
|
||||||
// https://on.cypress.io/filter
|
|
||||||
cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".find() - get descendant DOM elements of the selector", () => {
|
|
||||||
// https://on.cypress.io/find
|
|
||||||
cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".first() - get first DOM element", () => {
|
|
||||||
// https://on.cypress.io/first
|
|
||||||
cy.get(".traversal-table td").first().should("contain", "1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".last() - get last DOM element", () => {
|
|
||||||
// https://on.cypress.io/last
|
|
||||||
cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".next() - get next sibling DOM element", () => {
|
|
||||||
// https://on.cypress.io/next
|
|
||||||
cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".nextAll() - get all next sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/nextall
|
|
||||||
cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".nextUntil() - get next sibling DOM elements until next el", () => {
|
|
||||||
// https://on.cypress.io/nextuntil
|
|
||||||
cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".not() - remove DOM elements from set of DOM elements", () => {
|
|
||||||
// https://on.cypress.io/not
|
|
||||||
cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parent() - get parent DOM element from DOM elements", () => {
|
|
||||||
// https://on.cypress.io/parent
|
|
||||||
cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parents() - get parent DOM elements from DOM elements", () => {
|
|
||||||
// https://on.cypress.io/parents
|
|
||||||
cy.get(".traversal-cite").parents().should("match", "blockquote");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
|
|
||||||
// https://on.cypress.io/parentsuntil
|
|
||||||
cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prev() - get previous sibling DOM element", () => {
|
|
||||||
// https://on.cypress.io/prev
|
|
||||||
cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prevAll() - get all previous sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/prevall
|
|
||||||
cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prevUntil() - get all previous sibling DOM elements until el", () => {
|
|
||||||
// https://on.cypress.io/prevuntil
|
|
||||||
cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".siblings() - get all sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/siblings
|
|
||||||
cy.get(".traversal-pills .active").siblings().should("have.length", 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Utilities", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/utilities");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress._ - call a lodash method", () => {
|
|
||||||
// https://on.cypress.io/_
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
|
||||||
let ids = Cypress._.chain(response.body).map("id").take(3).value();
|
|
||||||
|
|
||||||
expect(ids).to.deep.eq([1, 2, 3]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.$ - call a jQuery method", () => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
let $li = Cypress.$(".utility-jquery li:first");
|
|
||||||
|
|
||||||
cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Blob - blob utilities and base64 string conversion", () => {
|
|
||||||
// https://on.cypress.io/blob
|
|
||||||
cy.get(".utility-blob").then(($div) => {
|
|
||||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
|
||||||
// get the dataUrl string for the javascript-logo
|
|
||||||
return Cypress.Blob.imgSrcToDataURL(
|
|
||||||
"https://example.cypress.io/assets/img/javascript-logo.png",
|
|
||||||
undefined,
|
|
||||||
"anonymous"
|
|
||||||
).then((dataUrl) => {
|
|
||||||
// create an <img> element and set its src to the dataUrl
|
|
||||||
let img = Cypress.$("<img />", { src: dataUrl });
|
|
||||||
|
|
||||||
// need to explicitly return cy here since we are initially returning
|
|
||||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
|
||||||
// append the image
|
|
||||||
$div.append(img);
|
|
||||||
|
|
||||||
cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.minimatch - test out glob patterns against strings", () => {
|
|
||||||
// https://on.cypress.io/minimatch
|
|
||||||
let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "matching wildcard").to.be.true;
|
|
||||||
|
|
||||||
matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.false;
|
|
||||||
|
|
||||||
// ** matches against all downstream path segments
|
|
||||||
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.true;
|
|
||||||
|
|
||||||
// whereas * matches only the next path segment
|
|
||||||
|
|
||||||
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
|
|
||||||
matchBase: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Promise - instantiate a bluebird promise", () => {
|
|
||||||
// https://on.cypress.io/promise
|
|
||||||
let waited = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Bluebird<string>
|
|
||||||
*/
|
|
||||||
function waitOneSecond() {
|
|
||||||
// return a promise that resolves after 1 second
|
|
||||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// set waited to true
|
|
||||||
waited = true;
|
|
||||||
|
|
||||||
// resolve with 'foo' string
|
|
||||||
resolve("foo");
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.then(() => {
|
|
||||||
// return a promise to cy.then() that
|
|
||||||
// is awaited until it resolves
|
|
||||||
// @ts-ignore TS7006
|
|
||||||
return waitOneSecond().then((str) => {
|
|
||||||
expect(str).to.eq("foo");
|
|
||||||
expect(waited).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Viewport", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/viewport");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.viewport() - set the viewport size and dimension", () => {
|
|
||||||
// https://on.cypress.io/viewport
|
|
||||||
|
|
||||||
cy.get("#navbar").should("be.visible");
|
|
||||||
cy.viewport(320, 480);
|
|
||||||
|
|
||||||
// the navbar should have collapse since our screen is smaller
|
|
||||||
cy.get("#navbar").should("not.be.visible");
|
|
||||||
cy.get(".navbar-toggle").should("be.visible").click();
|
|
||||||
cy.get(".nav").find("a").should("be.visible");
|
|
||||||
|
|
||||||
// lets see what our app looks like on a super large screen
|
|
||||||
cy.viewport(2999, 2999);
|
|
||||||
|
|
||||||
// cy.viewport() accepts a set of preset sizes
|
|
||||||
// to easily set the screen to a device's width and height
|
|
||||||
|
|
||||||
// We added a cy.wait() between each viewport change so you can see
|
|
||||||
// the change otherwise it is a little too fast to see :)
|
|
||||||
|
|
||||||
cy.viewport("macbook-15");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("macbook-13");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("macbook-11");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("ipad-2");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("ipad-mini");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-6+");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-6");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-5");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-4");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-3");
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// cy.viewport() accepts an orientation for all presets
|
|
||||||
// the default orientation is 'portrait'
|
|
||||||
cy.viewport("ipad-2", "portrait");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-4", "landscape");
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// The viewport will be reset back to the default dimensions
|
|
||||||
// in between tests (the default can be set in cypress.json)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Waiting", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/waiting");
|
|
||||||
});
|
|
||||||
// BE CAREFUL of adding unnecessary wait times.
|
|
||||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
it("cy.wait() - wait for a specific amount of time", () => {
|
|
||||||
cy.get(".wait-input1").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get(".wait-input2").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get(".wait-input3").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.wait() - wait for a specific route", () => {
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// wait for GET comments/1
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Window", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/window");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.window() - get the global window object", () => {
|
|
||||||
// https://on.cypress.io/window
|
|
||||||
cy.window().should("have.property", "top");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.document() - get the document object", () => {
|
|
||||||
// https://on.cypress.io/document
|
|
||||||
cy.document().should("have.property", "charset").and("eq", "UTF-8");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.title() - get the title", () => {
|
|
||||||
// https://on.cypress.io/title
|
|
||||||
cy.title().should("include", "Kitchen Sink");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"id": 8739,
|
|
||||||
"name": "Jane",
|
|
||||||
"email": "jane@example.com"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.jsx can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
||||||
|
|
||||||
import "@testing-library/cypress/add-commands";
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/index.jsx is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"baseUrl": "../node_modules",
|
|
||||||
"types": ["cypress"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.*"]
|
|
||||||
}
|
|
||||||
2982
client/package-lock.json
generated
2982
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,29 +2,34 @@
|
|||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.3",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.13.1",
|
"@apollo/client": "^3.13.5",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
|
"@firebase/analytics": "^0.10.12",
|
||||||
|
"@firebase/app": "^0.11.3",
|
||||||
|
"@firebase/auth": "^1.9.1",
|
||||||
|
"@firebase/firestore": "^4.7.10",
|
||||||
|
"@firebase/messaging": "^0.12.17",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.1",
|
||||||
"@sentry/cli": "^2.42.2",
|
"@sentry/cli": "^2.42.4",
|
||||||
"@sentry/react": "^9.3.0",
|
"@sentry/react": "^9.9.0",
|
||||||
"@sentry/vite-plugin": "^3.2.2",
|
"@sentry/vite-plugin": "^3.2.2",
|
||||||
"@splitsoftware/splitio-react": "^1.13.0",
|
"@splitsoftware/splitio-react": "^2.0.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.24.2",
|
"antd": "^5.24.5",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.1.0",
|
"apollo-link-sentry": "^4.2.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@@ -33,14 +38,13 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.13.2",
|
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.4",
|
"libphonenumber-js": "^1.12.6",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.3",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -50,15 +54,15 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.2",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^15.4.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^10.1.0",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
"react-popopo": "^2.1.9",
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.61",
|
"react-product-fruits": "^2.2.61",
|
||||||
@@ -74,12 +78,11 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.86.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.15",
|
"styled-components": "^6.1.16",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.8",
|
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -96,8 +99,6 @@
|
|||||||
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
||||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||||
"test": "cypress open",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
"eulaize": "node src/utils/eulaize.js"
|
"eulaize": "node src/utils/eulaize.js"
|
||||||
},
|
},
|
||||||
@@ -120,23 +121,19 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@dotenvx/dotenvx": "^1.38.3",
|
"@dotenvx/dotenvx": "^1.39.0",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/cypress": "^10.0.2",
|
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"cypress": "^13.17.0",
|
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
@@ -144,11 +141,11 @@
|
|||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.3",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^0.21.1",
|
"vite-plugin-pwa": "^0.21.2",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import React from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
import themeProvider from "./themeProvider";
|
import themeProvider from "./themeProvider";
|
||||||
import { Userpilot } from "userpilot";
|
|
||||||
|
|
||||||
// Initialize Userpilot
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
Userpilot.initialize("NX-69145f08");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Base Split configuration
|
||||||
const config = {
|
const config = {
|
||||||
core: {
|
core: {
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
key: "anon"
|
key: "anon" // Default key, overridden dynamically by SplitClientProvider
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const factory = SplitSdk(config);
|
|
||||||
|
// Custom provider to manage the Split client key based on imexshopid from Redux
|
||||||
|
function SplitClientProvider({ children }) {
|
||||||
|
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
||||||
|
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (splitClient && imexshopid) {
|
||||||
|
// Log readiness for debugging; no need for ready() since isReady is available
|
||||||
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
|
}
|
||||||
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
function AppContainer() {
|
function AppContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -31,7 +40,6 @@ function AppContainer() {
|
|||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
//componentSize="small"
|
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
theme={themeProvider}
|
theme={themeProvider}
|
||||||
@@ -43,8 +51,10 @@ function AppContainer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider factory={factory}>
|
<SplitFactoryProvider config={config}>
|
||||||
<App />
|
<SplitClientProvider>
|
||||||
|
<App />
|
||||||
|
</SplitClientProvider>
|
||||||
</SplitFactoryProvider>
|
</SplitFactoryProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
|||||||
132
client/src/components/dashboard-grid/componentList.js
Normal file
132
client/src/components/dashboard-grid/componentList.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import i18next from "i18next";
|
||||||
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||||
|
import {
|
||||||
|
DashboardTotalProductionHours,
|
||||||
|
DashboardTotalProductionHoursGql
|
||||||
|
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||||
|
import DashboardProjectedMonthlySales, {
|
||||||
|
DashboardProjectedMonthlySalesGql
|
||||||
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||||
|
import DashboardMonthlyRevenueGraph, {
|
||||||
|
DashboardMonthlyRevenueGraphGql
|
||||||
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||||
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||||
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||||
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||||
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
|
DashboardMonthlyEmployeeEfficiencyGql
|
||||||
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
||||||
|
import DashboardScheduledInToday, {
|
||||||
|
DashboardScheduledInTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||||
|
import DashboardScheduledOutToday, {
|
||||||
|
DashboardScheduledOutTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||||
|
import JobLifecycleDashboardComponent, {
|
||||||
|
JobLifecycleDashboardGQL
|
||||||
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||||
|
|
||||||
|
const componentList = {
|
||||||
|
ProductionDollars: {
|
||||||
|
label: i18next.t("dashboard.titles.productiondollars"),
|
||||||
|
component: DashboardTotalProductionDollars,
|
||||||
|
gqlFragment: null,
|
||||||
|
w: 1,
|
||||||
|
h: 1,
|
||||||
|
minW: 2,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
ProductionHours: {
|
||||||
|
label: i18next.t("dashboard.titles.productionhours"),
|
||||||
|
component: DashboardTotalProductionHours,
|
||||||
|
gqlFragment: DashboardTotalProductionHoursGql,
|
||||||
|
w: 3,
|
||||||
|
h: 1,
|
||||||
|
minW: 3,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
ProjectedMonthlySales: {
|
||||||
|
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||||
|
component: DashboardProjectedMonthlySales,
|
||||||
|
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
minW: 2,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
MonthlyRevenueGraph: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||||
|
component: DashboardMonthlyRevenueGraph,
|
||||||
|
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||||
|
w: 4,
|
||||||
|
h: 2,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2
|
||||||
|
},
|
||||||
|
MonthlyJobCosting: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||||
|
component: DashboardMonthlyJobCosting,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
MonthlyPartsSales: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||||
|
component: DashboardMonthlyPartsSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
MonthlyLaborSales: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||||
|
component: DashboardMonthlyLaborSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
||||||
|
MonthlyEmployeeEfficency: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||||
|
component: DashboardMonthlyEmployeeEfficiency,
|
||||||
|
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
ScheduleInToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||||
|
component: DashboardScheduledInToday,
|
||||||
|
gqlFragment: DashboardScheduledInTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
ScheduleOutToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||||
|
component: DashboardScheduledOutToday,
|
||||||
|
gqlFragment: DashboardScheduledOutTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
JobLifecycle: {
|
||||||
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||||
|
component: JobLifecycleDashboardComponent,
|
||||||
|
gqlFragment: JobLifecycleDashboardGQL,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default componentList;
|
||||||
85
client/src/components/dashboard-grid/createDashboardQuery.js
Normal file
85
client/src/components/dashboard-grid/createDashboardQuery.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
import dayjs from "../../utils/day.js";
|
||||||
|
import componentList from "./componentList.js";
|
||||||
|
|
||||||
|
const createDashboardQuery = (state) => {
|
||||||
|
const componentBasedAdditions =
|
||||||
|
state &&
|
||||||
|
Array.isArray(state.layout) &&
|
||||||
|
state.layout.map((item, index) => componentList[item.i].gqlFragment || "").join("");
|
||||||
|
return gql`
|
||||||
|
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||||
|
monthly_sales: jobs(where: {_and: [
|
||||||
|
{ voided: {_eq: false}},
|
||||||
|
{date_invoiced: {_gte: "${dayjs()
|
||||||
|
.startOf("month")
|
||||||
|
.startOf("day")
|
||||||
|
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs().endOf("month").endOf("day").toISOString()}"}}]}) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
date_invoiced
|
||||||
|
job_totals
|
||||||
|
rate_la1
|
||||||
|
rate_la2
|
||||||
|
rate_la3
|
||||||
|
rate_la4
|
||||||
|
rate_laa
|
||||||
|
rate_lab
|
||||||
|
rate_lad
|
||||||
|
rate_lae
|
||||||
|
rate_laf
|
||||||
|
rate_lag
|
||||||
|
rate_lam
|
||||||
|
rate_lar
|
||||||
|
rate_las
|
||||||
|
rate_lau
|
||||||
|
rate_ma2s
|
||||||
|
rate_ma2t
|
||||||
|
rate_ma3s
|
||||||
|
rate_mabl
|
||||||
|
rate_macs
|
||||||
|
rate_mahw
|
||||||
|
rate_mapa
|
||||||
|
rate_mash
|
||||||
|
rate_matd
|
||||||
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
|
id
|
||||||
|
mod_lbr_ty
|
||||||
|
mod_lb_hrs
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
part_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
ins_co_nm
|
||||||
|
job_totals
|
||||||
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
|
id
|
||||||
|
mod_lbr_ty
|
||||||
|
mod_lb_hrs
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
part_type
|
||||||
|
}
|
||||||
|
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createDashboardQuery;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
import { isEmpty, cloneDeep } from "lodash";
|
||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Dropdown, Space } from "antd";
|
import { Button, Dropdown, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import i18next from "i18next";
|
import { useMemo, useState } from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdClose } from "react-icons/md";
|
import { MdClose } from "react-icons/md";
|
||||||
@@ -15,38 +13,13 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import DashboardMonthlyEmployeeEfficiency, {
|
|
||||||
DashboardMonthlyEmployeeEfficiencyGql
|
|
||||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
|
||||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
|
||||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
|
||||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
|
||||||
import DashboardMonthlyRevenueGraph, {
|
|
||||||
DashboardMonthlyRevenueGraphGql
|
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
|
||||||
import DashboardProjectedMonthlySales, {
|
|
||||||
DashboardProjectedMonthlySalesGql
|
|
||||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
|
||||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
|
||||||
import DashboardTotalProductionHours, {
|
|
||||||
DashboardTotalProductionHoursGql
|
|
||||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
//Combination of the following:
|
|
||||||
// /node_modules/react-grid-layout/css/styles.css
|
|
||||||
// /node_modules/react-resizable/css/styles.css
|
|
||||||
import DashboardScheduledInToday, {
|
|
||||||
DashboardScheduledInTodayGql
|
|
||||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
|
|
||||||
import DashboardScheduledOutToday, {
|
|
||||||
DashboardScheduledOutTodayGql
|
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
|
||||||
import JobLifecycleDashboardComponent, {
|
|
||||||
JobLifecycleDashboardGQL
|
|
||||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
|
|
||||||
import "./dashboard-grid.styles.scss";
|
|
||||||
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import componentList from "./componentList.js";
|
||||||
|
import createDashboardQuery from "./createDashboardQuery.js";
|
||||||
|
|
||||||
|
import "./dashboard-grid.styles.scss";
|
||||||
|
|
||||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
@@ -54,6 +27,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
@@ -85,19 +59,21 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
layout: { ...state, layout, layouts }
|
layout: { ...state, layout, layouts }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!result.errors) {
|
|
||||||
notification["error"]({
|
if (!isEmpty(result?.errors)) {
|
||||||
|
notification.error({
|
||||||
message: t("dashboard.errors.updatinglayout", {
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
message: JSON.stringify(result.errors)
|
message: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveComponent = (key) => {
|
const handleRemoveComponent = (key) => {
|
||||||
logImEXEvent("dashboard_remove_component", { name: key });
|
logImEXEvent("dashboard_remove_component", { name: key });
|
||||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||||
|
|
||||||
const items = _.cloneDeep(state.items);
|
const items = cloneDeep(state.items);
|
||||||
|
|
||||||
items.splice(idxToRemove, 1);
|
items.splice(idxToRemove, 1);
|
||||||
setState({ ...state, items });
|
setState({ ...state, items });
|
||||||
@@ -120,7 +96,8 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dashboarddata = React.useMemo(() => GenerateDashboardData(data), [data]);
|
const dashboardData = useMemo(() => GenerateDashboardData(data), [data]);
|
||||||
|
|
||||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||||
|
|
||||||
const menuItems = Object.keys(componentList).map((key) => ({
|
const menuItems = Object.keys(componentList).map((key) => ({
|
||||||
@@ -156,7 +133,6 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
layouts={state.layouts}
|
layouts={state.layouts}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
// onBreakpointChange={onBreakpointChange}
|
|
||||||
>
|
>
|
||||||
{state.items.map((item, index) => {
|
{state.items.map((item, index) => {
|
||||||
const TheComponent = componentList[item.i].component;
|
const TheComponent = componentList[item.i].component;
|
||||||
@@ -182,7 +158,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
}}
|
}}
|
||||||
onClick={() => handleRemoveComponent(item.i)}
|
onClick={() => handleRemoveComponent(item.i)}
|
||||||
/>
|
/>
|
||||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata} />
|
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />
|
||||||
</LoadingSkeleton>
|
</LoadingSkeleton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -193,189 +169,3 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);
|
||||||
|
|
||||||
const componentList = {
|
|
||||||
ProductionDollars: {
|
|
||||||
label: i18next.t("dashboard.titles.productiondollars"),
|
|
||||||
component: DashboardTotalProductionDollars,
|
|
||||||
gqlFragment: null,
|
|
||||||
w: 1,
|
|
||||||
h: 1,
|
|
||||||
minW: 2,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
ProductionHours: {
|
|
||||||
label: i18next.t("dashboard.titles.productionhours"),
|
|
||||||
component: DashboardTotalProductionHours,
|
|
||||||
gqlFragment: DashboardTotalProductionHoursGql,
|
|
||||||
w: 3,
|
|
||||||
h: 1,
|
|
||||||
minW: 3,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
ProjectedMonthlySales: {
|
|
||||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
|
||||||
component: DashboardProjectedMonthlySales,
|
|
||||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
|
||||||
w: 2,
|
|
||||||
h: 1,
|
|
||||||
minW: 2,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
MonthlyRevenueGraph: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
|
||||||
component: DashboardMonthlyRevenueGraph,
|
|
||||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
|
||||||
w: 4,
|
|
||||||
h: 2,
|
|
||||||
minW: 4,
|
|
||||||
minH: 2
|
|
||||||
},
|
|
||||||
MonthlyJobCosting: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
|
||||||
component: DashboardMonthlyJobCosting,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 6,
|
|
||||||
minH: 3,
|
|
||||||
w: 6,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
MonthlyPartsSales: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
|
||||||
component: DashboardMonthlyPartsSales,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
MonthlyLaborSales: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
|
||||||
component: DashboardMonthlyLaborSales,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
|
||||||
MonthlyEmployeeEfficency: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
|
||||||
component: DashboardMonthlyEmployeeEfficiency,
|
|
||||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
ScheduleInToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
|
||||||
component: DashboardScheduledInToday,
|
|
||||||
gqlFragment: DashboardScheduledInTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
ScheduleOutToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
|
||||||
component: DashboardScheduledOutToday,
|
|
||||||
gqlFragment: DashboardScheduledOutTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
JobLifecycle: {
|
|
||||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
|
||||||
component: JobLifecycleDashboardComponent,
|
|
||||||
gqlFragment: JobLifecycleDashboardGQL,
|
|
||||||
minW: 6,
|
|
||||||
minH: 3,
|
|
||||||
w: 6,
|
|
||||||
h: 3
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDashboardQuery = (state) => {
|
|
||||||
const componentBasedAdditions =
|
|
||||||
state &&
|
|
||||||
Array.isArray(state.layout) &&
|
|
||||||
state.layout.map((item, index) => componentList[item.i].gqlFragment || "").join("");
|
|
||||||
return gql`
|
|
||||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
|
||||||
monthly_sales: jobs(where: {_and: [
|
|
||||||
{ voided: {_eq: false}},
|
|
||||||
{date_invoiced: {_gte: "${dayjs()
|
|
||||||
.startOf("month")
|
|
||||||
.startOf("day")
|
|
||||||
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
|
|
||||||
.endOf("month")
|
|
||||||
.endOf("day")
|
|
||||||
.toISOString()}"}}]}) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
date_invoiced
|
|
||||||
job_totals
|
|
||||||
rate_la1
|
|
||||||
rate_la2
|
|
||||||
rate_la3
|
|
||||||
rate_la4
|
|
||||||
rate_laa
|
|
||||||
rate_lab
|
|
||||||
rate_lad
|
|
||||||
rate_lae
|
|
||||||
rate_laf
|
|
||||||
rate_lag
|
|
||||||
rate_lam
|
|
||||||
rate_lar
|
|
||||||
rate_las
|
|
||||||
rate_lau
|
|
||||||
rate_ma2s
|
|
||||||
rate_ma2t
|
|
||||||
rate_ma3s
|
|
||||||
rate_mabl
|
|
||||||
rate_macs
|
|
||||||
rate_mahw
|
|
||||||
rate_mapa
|
|
||||||
rate_mash
|
|
||||||
rate_matd
|
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
|
||||||
id
|
|
||||||
mod_lbr_ty
|
|
||||||
mod_lb_hrs
|
|
||||||
act_price
|
|
||||||
part_qty
|
|
||||||
part_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
ins_co_nm
|
|
||||||
job_totals
|
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
|
||||||
id
|
|
||||||
mod_lbr_ty
|
|
||||||
mod_lb_hrs
|
|
||||||
act_price
|
|
||||||
part_qty
|
|
||||||
part_type
|
|
||||||
}
|
|
||||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||||
import client from "../../utils/GraphQLClient";
|
import client from "../../utils/GraphQLClient";
|
||||||
import { error } from "logrocket";
|
|
||||||
|
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PauseCircleOutlined, PlayCircleOutlined, PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Card, Col, Divider, Drawer, Grid, Row, Space } from "antd";
|
import { Button, Card, Col, Divider, Drawer, Grid, Row, Space } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
||||||
|
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
||||||
@@ -21,15 +25,21 @@ import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.compo
|
|||||||
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
||||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
||||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const span = {
|
const span = {
|
||||||
@@ -38,8 +48,9 @@ const span = {
|
|||||||
xxl: { span: 8 }
|
xxl: { span: 8 }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTrail }) {
|
||||||
const { scenarioNotificationsOn } = useSocket();
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
.slice(-1)[0];
|
.slice(-1)[0];
|
||||||
@@ -91,7 +102,29 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<JobSyncButton job={data.jobs_by_pk} />
|
<JobSyncButton job={data.jobs_by_pk} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("production_toggle_alert");
|
||||||
|
updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: data.jobs_by_pk.id,
|
||||||
|
job: {
|
||||||
|
suspended: !data.jobs_by_pk.suspended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: data.jobs_by_pk.id,
|
||||||
|
operation: AuditTrailMapping.jobsuspend(
|
||||||
|
data.jobs_by_pk.suspended ? !data.jobs_by_pk.suspended : true
|
||||||
|
),
|
||||||
|
type: "jobsuspend"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={data.jobs_by_pk.suspended ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
|
>
|
||||||
|
{data.jobs_by_pk.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
@@ -103,8 +136,8 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Col, Row, Space } from "antd";
|
import { Button, Card, Col, Row, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import i18n from "i18next";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Gallery } from "react-grid-gallery";
|
import { Gallery } from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Lightbox from "react-image-lightbox";
|
import Lightbox from "react-image-lightbox";
|
||||||
@@ -16,8 +18,6 @@ import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.downlo
|
|||||||
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
||||||
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
||||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
||||||
import i18n from "i18next";
|
|
||||||
import { isFunction } from "lodash";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -45,15 +45,15 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||||
|
|
||||||
const fetchThumbnails = () => {
|
const fetchThumbnails = useCallback(() => {
|
||||||
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
||||||
};
|
}, [jobId, setGalleryImages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
fetchThumbnails();
|
fetchThumbnails();
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, fetchThumbnails]);
|
||||||
|
|
||||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||||
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Gallery } from "react-grid-gallery";
|
import { Gallery } from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.component";
|
import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.component";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -13,15 +12,10 @@ import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.compo
|
|||||||
|
|
||||||
function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState }) {
|
function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState }) {
|
||||||
const [galleryImages, setgalleryImages] = externalMediaState;
|
const [galleryImages, setgalleryImages] = externalMediaState;
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const fetchThumbnails = () => {
|
|
||||||
fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchThumbnails();
|
if (jobId) fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
||||||
}, [jobId]);
|
}, [jobId, setgalleryImages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="clearfix">
|
<div className="clearfix">
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||||
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -32,7 +33,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item }) => {
|
const handleClick = ({ item, key, keyPath }) => {
|
||||||
form.setFieldsValue({ comments: item.props.value });
|
form.setFieldsValue({ comments: item.props.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,18 +98,17 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
<Checkbox />
|
<Checkbox />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{!isReturn && (
|
|
||||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||||
<Radio.Group disabled={sendType === "oec"}>
|
<Radio.Group disabled={sendType === "oec"}>
|
||||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
||||||
<Form.List name={["parts_order_lines", "data"]}>
|
<Form.List name={["parts_order_lines", "data"]}>
|
||||||
{(fields, { remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PauseCircleOutlined, PlayCircleOutlined, PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import { Button, Descriptions, Drawer, Space } from "antd";
|
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { Button, Descriptions, Drawer, Space } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||||
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
@@ -24,22 +27,29 @@ import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.p
|
|||||||
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
|
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
|
||||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||||
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||||
|
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
||||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
technician: selectTechnician
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListDetail);
|
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListDetail);
|
||||||
|
|
||||||
export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, technician }) {
|
export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, technician, insertAuditTrail }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const { selected } = search;
|
const { selected } = search;
|
||||||
@@ -58,6 +68,7 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -72,6 +83,29 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
||||||
|
{!technician && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("production_toggle_alert");
|
||||||
|
updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: theJob.id,
|
||||||
|
job: {
|
||||||
|
suspended: !theJob.suspended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: theJob.id,
|
||||||
|
operation: AuditTrailMapping.jobsuspend(theJob.suspended ? !theJob.suspended : true),
|
||||||
|
type: "jobsuspend"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={theJob.suspended ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
|
>
|
||||||
|
{theJob.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
@@ -83,8 +117,8 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
{!technician ? <ScoreboardAddButton job={data ? data.jobs_by_pk : {}} /> : null}
|
{!technician ? <ScoreboardAddButton job={data ? data.jobs_by_pk : {}} /> : null}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { QUERY_ALL_ASSOCIATIONS, UPDATE_ACTIVE_ASSOCIATION } from "../../graphql
|
|||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ProfileShopsComponent from "./profile-shops.component";
|
import ProfileShopsComponent from "./profile-shops.component";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getToken } from "firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getAnalytics, logEvent } from "firebase/analytics";
|
import { getAnalytics, logEvent } from "@firebase/analytics";
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "@firebase/app";
|
||||||
import { getAuth, updatePassword, updateProfile } from "firebase/auth";
|
import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
|
||||||
import { getFirestore } from "firebase/firestore";
|
import { getFirestore } from "@firebase/firestore";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||||
import { store } from "../redux/store";
|
import { store } from "../redux/store";
|
||||||
|
|
||||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default connect(mapStateToProps, null)(LandingPage);
|
|||||||
|
|
||||||
export function LandingPage({ currentUser }) {
|
export function LandingPage({ currentUser }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
console.log("Main");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigate("/manage/jobs");
|
navigate("/manage/jobs");
|
||||||
}, [currentUser, navigate]);
|
}, [currentUser, navigate]);
|
||||||
|
|||||||
@@ -118,3 +118,8 @@ export const setCurrentEula = (eula) => ({
|
|||||||
export const acceptEula = () => ({
|
export const acceptEula = () => ({
|
||||||
type: UserActionTypes.EULA_ACCEPTED
|
type: UserActionTypes.EULA_ACCEPTED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setImexShopId = (imexshopid) => ({
|
||||||
|
type: UserActionTypes.SET_IMEX_SHOP_ID,
|
||||||
|
payload: imexshopid
|
||||||
|
});
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
};
|
};
|
||||||
case UserActionTypes.SET_AUTH_LEVEL:
|
case UserActionTypes.SET_AUTH_LEVEL:
|
||||||
return { ...state, authLevel: action.payload };
|
return { ...state, authLevel: action.payload };
|
||||||
|
case UserActionTypes.SET_IMEX_SHOP_ID:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
imexshopid: action.payload
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
|||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { setUserId, setUserProperties } from "firebase/analytics";
|
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
confirmPasswordReset,
|
confirmPasswordReset,
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
signInWithEmailAndPassword,
|
signInWithEmailAndPassword,
|
||||||
signOut
|
signOut
|
||||||
} from "firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import { factory } from "../../App/App.container";
|
|
||||||
import {
|
import {
|
||||||
analytics,
|
analytics,
|
||||||
auth,
|
auth,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
sendPasswordResetFailure,
|
sendPasswordResetFailure,
|
||||||
sendPasswordResetSuccess,
|
sendPasswordResetSuccess,
|
||||||
setAuthlevel,
|
setAuthlevel,
|
||||||
|
setImexShopId,
|
||||||
setInstanceConflict,
|
setInstanceConflict,
|
||||||
setInstanceId,
|
setInstanceId,
|
||||||
setLocalFingerprint,
|
setLocalFingerprint,
|
||||||
@@ -318,7 +318,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory.client(payload.imexshopid);
|
// Dispatch the imexshopid to Redux store
|
||||||
|
yield put(setImexShopId(payload.imexshopid));
|
||||||
|
|
||||||
const authRecord = payload.associations.filter((a) => a.useremail.toLowerCase() === userEmail.toLowerCase());
|
const authRecord = payload.associations.filter((a) => a.useremail.toLowerCase() === userEmail.toLowerCase());
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const UserActionTypes = {
|
|||||||
CHECK_ACTION_CODE_SUCCESS: "CHECK_ACTION_CODE_SUCCESS",
|
CHECK_ACTION_CODE_SUCCESS: "CHECK_ACTION_CODE_SUCCESS",
|
||||||
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
|
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
|
||||||
SET_CURRENT_EULA: "SET_CURRENT_EULA",
|
SET_CURRENT_EULA: "SET_CURRENT_EULA",
|
||||||
EULA_ACCEPTED: "EULA_ACCEPTED"
|
EULA_ACCEPTED: "EULA_ACCEPTED",
|
||||||
|
SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID"
|
||||||
};
|
};
|
||||||
export default UserActionTypes;
|
export default UserActionTypes;
|
||||||
|
|||||||
@@ -191,24 +191,11 @@ export default defineConfig({
|
|||||||
"@sentry/react": ["@sentry/react"],
|
"@sentry/react": ["@sentry/react"],
|
||||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||||
logrocket: ["logrocket"],
|
logrocket: ["logrocket"],
|
||||||
"firebase/app": ["firebase/app"],
|
"@firebase/analytics": ["@firebase/analytics"],
|
||||||
"firebase/firestore": ["firebase/firestore"],
|
|
||||||
"firebase/firestore/lite": ["firebase/firestore/lite"],
|
|
||||||
"firebase/auth": ["firebase/auth"],
|
|
||||||
"firebase/functions": ["firebase/functions"],
|
|
||||||
"firebase/storage": ["firebase/storage"],
|
|
||||||
"firebase/database": ["firebase/database"],
|
|
||||||
"firebase/remote-config": ["firebase/remote-config"],
|
|
||||||
"firebase/performance": ["firebase/performance"],
|
|
||||||
"@firebase/app": ["@firebase/app"],
|
"@firebase/app": ["@firebase/app"],
|
||||||
"@firebase/firestore": ["@firebase/firestore"],
|
"@firebase/firestore": ["@firebase/firestore"],
|
||||||
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
|
|
||||||
"@firebase/auth": ["@firebase/auth"],
|
"@firebase/auth": ["@firebase/auth"],
|
||||||
"@firebase/functions": ["@firebase/functions"],
|
"@firebase/messaging": ["@firebase/messaging"],
|
||||||
"@firebase/storage": ["@firebase/storage"],
|
|
||||||
"@firebase/database": ["@firebase/database"],
|
|
||||||
"@firebase/remote-config": ["@firebase/remote-config"],
|
|
||||||
"@firebase/performance": ["@firebase/performance"],
|
|
||||||
markerjs2: ["markerjs2"],
|
markerjs2: ["markerjs2"],
|
||||||
"@apollo/client": ["@apollo/client"],
|
"@apollo/client": ["@apollo/client"],
|
||||||
"libphonenumber-js": ["libphonenumber-js"]
|
"libphonenumber-js": ["libphonenumber-js"]
|
||||||
|
|||||||
2286
package-lock.json
generated
2286
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -9,59 +9,53 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
||||||
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
||||||
"admin": "cd admin && npm start",
|
|
||||||
"client": "cd client && npm start",
|
|
||||||
"server": "nodemon server.js",
|
|
||||||
"build": "cd client && npm run build",
|
|
||||||
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
|
|
||||||
"deva": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\" \"npm run admin\"",
|
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.738.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.758.0",
|
"@aws-sdk/client-elasticache": "^3.772.0",
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.772.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
||||||
"@aws-sdk/client-ses": "^3.738.0",
|
"@aws-sdk/client-ses": "^3.772.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.758.0",
|
"@aws-sdk/credential-provider-node": "^3.772.0",
|
||||||
"@aws-sdk/lib-storage": "^3.743.0",
|
"@aws-sdk/lib-storage": "^3.774.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.731.1",
|
"@aws-sdk/s3-request-presigner": "^3.774.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.4",
|
||||||
"bee-queue": "^1.7.1",
|
"bee-queue": "^1.7.1",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"bullmq": "^5.41.7",
|
"bullmq": "^5.44.4",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.6.0",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dd-trace": "^5.40.0",
|
"dd-trace": "^5.43.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.1.0",
|
"firebase-admin": "^13.2.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.3",
|
"inline-css": "^4.0.3",
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.5.0",
|
"ioredis": "^5.6.0",
|
||||||
"json-2-csv": "^5.5.8",
|
"json-2-csv": "^5.5.9",
|
||||||
"juice": "^11.0.1",
|
"juice": "^11.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.47",
|
"moment-timezone": "^0.5.48",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.6",
|
"node-mailjet": "^6.0.8",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.58",
|
"phone": "^3.1.58",
|
||||||
@@ -69,22 +63,21 @@
|
|||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
"soap": "^1.1.9",
|
"soap": "^1.1.10",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^4.23.0",
|
"twilio": "^5.5.1",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"concurrently": "^8.2.2",
|
"eslint": "^9.23.0",
|
||||||
"eslint": "^9.21.0",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const AxiosLib = require("axios").default;
|
|||||||
const axios = AxiosLib.create();
|
const axios = AxiosLib.create();
|
||||||
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
||||||
const { CheckForErrors } = require("./pbs-job-export");
|
const { CheckForErrors } = require("./pbs-job-export");
|
||||||
const uuid = require("uuid").v4;
|
|
||||||
axios.interceptors.request.use((x) => {
|
axios.interceptors.request.use((x) => {
|
||||||
const socket = x.socket;
|
const socket = x.socket;
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ axios.interceptors.request.use((x) => {
|
|||||||
...x.headers[x.method],
|
...x.headers[x.method],
|
||||||
...x.headers
|
...x.headers
|
||||||
};
|
};
|
||||||
|
|
||||||
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
||||||
x.url
|
x.url
|
||||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const entegralEndpoint =
|
|||||||
: "https://uat-ws.armsbusinesssolutions.net/RepairOrderFolderService/RepairOrderFolderService.asmx?WSDL";
|
: "https://uat-ws.armsbusinesssolutions.net/RepairOrderFolderService/RepairOrderFolderService.asmx?WSDL";
|
||||||
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const uuid = require("uuid").v4;
|
const { v4 } = require("uuid");
|
||||||
|
|
||||||
const momentFormat = "yyyy-MM-DDTHH:mm:ss.SSS";
|
const momentFormat = "yyyy-MM-DDTHH:mm:ss.SSS";
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transId = uuid(); // Can this actually be the job id?
|
const transId = v4(); // Can this actually be the job id?
|
||||||
let obj = {
|
let obj = {
|
||||||
RqUID: transId,
|
RqUID: transId,
|
||||||
DocumentInfo: {
|
DocumentInfo: {
|
||||||
|
|||||||
@@ -2708,66 +2708,6 @@ exports.INSERT_AUDIT_TRAIL = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports.GET_DOCUMENTS_BY_JOB = `
|
|
||||||
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
|
||||||
jobs_by_pk(id: $jobId) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
}
|
|
||||||
documents_aggregate(where: { jobid: { _eq: $jobId } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
documents(order_by: { takenat: desc }, where: { jobid: { _eq: $jobId } }) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
key
|
|
||||||
type
|
|
||||||
size
|
|
||||||
takenat
|
|
||||||
extension
|
|
||||||
bill {
|
|
||||||
id
|
|
||||||
invoice_number
|
|
||||||
date
|
|
||||||
vendor {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
exports.QUERY_TEMPORARY_DOCS = ` query QUERY_TEMPORARY_DOCS {
|
|
||||||
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
key
|
|
||||||
type
|
|
||||||
extension
|
|
||||||
size
|
|
||||||
takenat
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
exports.GET_DOCUMENTS_BY_IDS = `
|
|
||||||
query GET_DOCUMENTS_BY_IDS($documentIds: [uuid!]!) {
|
|
||||||
documents(where: {id: {_in: $documentIds}}, order_by: {takenat: desc}) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
key
|
|
||||||
type
|
|
||||||
extension
|
|
||||||
size
|
|
||||||
takenat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports.GET_JOB_WATCHERS = `
|
exports.GET_JOB_WATCHERS = `
|
||||||
query GET_JOB_WATCHERS($jobid: uuid!) {
|
query GET_JOB_WATCHERS($jobid: uuid!) {
|
||||||
job_watchers(where: { jobid: { _eq: $jobid } }) {
|
job_watchers(where: { jobid: { _eq: $jobid } }) {
|
||||||
@@ -2831,3 +2771,61 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.GET_DOCUMENTS_BY_JOB = `
|
||||||
|
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
||||||
|
jobs_by_pk(id: $jobId) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
documents_aggregate(where: { jobid: { _eq: $jobId } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documents(order_by: { takenat: desc }, where: { jobid: { _eq: $jobId } }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
extension
|
||||||
|
bill {
|
||||||
|
id
|
||||||
|
invoice_number
|
||||||
|
date
|
||||||
|
vendor {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.QUERY_TEMPORARY_DOCS = ` query QUERY_TEMPORARY_DOCS {
|
||||||
|
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
extension
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.GET_DOCUMENTS_BY_IDS = `
|
||||||
|
query GET_DOCUMENTS_BY_IDS($documentIds: [uuid!]!) {
|
||||||
|
documents(where: {id: {_in: $documentIds}}, order_by: {takenat: desc}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
extension
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ async function OpenSearchUpdateHandler(req, res) {
|
|||||||
res.status(200).json(response.body);
|
res.status(200).json(response.body);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// We don't want this spam message existing in development/test,
|
||||||
|
if (process.env?.NODE_ENV !== "production" && error?.message === "Invalid URL") {
|
||||||
|
return res.status(400).json(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
|
||||||
logger.log("os-handler-error", "ERROR", null, null, {
|
logger.log("os-handler-error", "ERROR", null, null, {
|
||||||
id: req.body.event.data.new.id,
|
id: req.body.event.data.new.id,
|
||||||
index: req.body.table.name,
|
index: req.body.table.name,
|
||||||
@@ -167,6 +172,7 @@ async function OpenSearchUpdateHandler(req, res) {
|
|||||||
stack: error.stack
|
stack: error.stack
|
||||||
// body: document
|
// body: document
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(400).json(JSON.stringify(error));
|
res.status(400).json(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
|||||||
const { canvastest } = require("../render/canvas-handler");
|
const { canvastest } = require("../render/canvas-handler");
|
||||||
const { alertCheck } = require("../alerts/alertcheck");
|
const { alertCheck } = require("../alerts/alertcheck");
|
||||||
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
||||||
const uuid = require("uuid").v4;
|
const { v4 } = require("uuid");
|
||||||
|
|
||||||
//Test route to ensure Express is responding.
|
//Test route to ensure Express is responding.
|
||||||
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
||||||
@@ -83,7 +83,7 @@ router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
|||||||
// image_path: [],
|
// image_path: [],
|
||||||
newMessage: {
|
newMessage: {
|
||||||
conversation: {
|
conversation: {
|
||||||
id: uuid(),
|
id: v4(),
|
||||||
archived: false,
|
archived: false,
|
||||||
bodyshop: {
|
bodyshop: {
|
||||||
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { status, markConversationRead } = require("../sms/status");
|
|||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
|
|
||||||
// Twilio Webhook Middleware for production
|
// Twilio Webhook Middleware for production
|
||||||
|
// TODO: Look into this because it technically is never validating anything
|
||||||
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
||||||
|
|
||||||
router.post("/receive", twilioWebhookMiddleware, receive);
|
router.post("/receive", twilioWebhookMiddleware, receive);
|
||||||
|
|||||||
Reference in New Issue
Block a user