Compare commits

...

39 Commits

Author SHA1 Message Date
swtmply
692f5c0167 IO-2327 timetickets test cases 2023-08-30 12:11:31 +08:00
swtmply
7fc9bd5b35 IO-2327 timetickets test cases 2023-08-29 11:17:36 +08:00
swtmply
86fdcdd397 IO-2327 timetickets test cases 2023-08-28 14:30:49 +08:00
swtmply
623b27dbab IO-2327 timetickets test cases 2023-08-25 11:56:24 +08:00
swtmply
c4a961fc03 IO-2327 timetickets test cases 2023-08-24 12:03:36 +08:00
swtmply
eb075d732a IO-2327 moved the describe to different files 2023-08-24 04:55:28 +08:00
swtmply
e7887ea822 IO-2327 added auto part ordering 2023-08-23 04:22:40 +08:00
swtmply
e74ba0942e IO-2327 posting bills test cases 2023-08-21 15:07:04 +08:00
swtmply
e89145e85f IO-2327 added some test cases on posting bills 2023-08-18 11:51:56 +08:00
swtmply
807dbf0248 IO-2327 parts ordering test case 2023-08-17 12:02:41 +08:00
swtmply
5104b27611 IO-2327 posting bill test cases 2023-08-16 10:57:55 +08:00
swtmply
619e7ca469 IO-2327 posting bill test cases 2023-08-15 12:05:10 +08:00
swtmply
8bddfdc104 IO-2327 posting bills test cases 2023-08-14 08:40:13 +08:00
swtmply
413af6eb7c IO-2327 posting bills test cases 2023-08-11 20:08:43 +08:00
swtmply
7b8691935d IO-2327 posting bill cases 2023-08-10 12:12:51 +08:00
swtmply
73baad21e2 IO-2327 posting bills test cases 2023-08-10 12:12:39 +08:00
swtmply
ffbd98eb4f IO-2327 posting bills test cases 2023-08-09 19:33:25 +08:00
swtmply
2cf4853960 IO-2327 posting bills test cases 2023-08-08 12:44:36 +08:00
swtmply
6db0947454 IO-2327 tax assertion 2023-08-05 12:01:49 +08:00
swtmply
59d9087ca7 IO-2327 tax assertion 2023-08-05 12:01:35 +08:00
swtmply
e4671b9bd6 IO-2327 posting bills test cases 2023-08-04 12:08:34 +08:00
swtmply
353d6d010c IO-2327 part ordering test cases 2023-08-04 04:36:16 +08:00
swtmply
b7366d7fc6 IO-2327 checklists test cases 2023-08-02 11:56:03 +08:00
swtmply
d12ff11deb IO-2327 updated test cases for intake checklist 2023-07-29 17:21:00 +08:00
swtmply
8b4ce18409 slider test component 2023-07-28 12:08:29 +08:00
swtmply
e4890c5fb8 job converting and importing test cases 2023-07-28 10:08:26 +08:00
swtmply
72c9b5a11e IO-2327 job import test cases 2023-07-21 11:38:57 +08:00
swtmply
835b90bc4b IO-2327 job importing test cases 2023-07-21 04:25:53 +08:00
swtmply
6562fd57df IO-2327 timetickets validation 2023-07-11 06:16:07 +08:00
swtmply
0a8c2bf385 IO-2327 payments test cases 2023-07-05 07:32:46 +08:00
swtmply
fc09871a47 IO-2327 posting bills tests cases 2023-07-03 09:06:58 +08:00
swtmply
39f7a3c870 IO-2327 posting bills test caes 2023-06-26 10:34:33 +08:00
swtmply
12d07f8318 IO-2327 parts ordering send by email 2023-06-22 06:43:45 +08:00
swtmply
7ae48db212 IO-2327 Intake checklist tests 2023-06-20 09:12:36 +08:00
swtmply
aa064cc573 IO-2327 job importing tests 2023-06-19 07:54:49 +08:00
swtmply
63f86e4627 IO-2327 importing job from available jobs table 2023-06-15 03:17:36 +08:00
swtmply
76b17cd249 IO-2327 converting a job tests 2023-06-15 00:22:17 +08:00
swtmply
8f8fdf7548 IO-2327 tests for login and reset password 2023-06-14 04:17:25 +08:00
Patrick Fic
058edca573 Remove old cypress config & migrate. 2023-06-07 08:35:30 -07:00
110 changed files with 18053 additions and 2405 deletions

4
.gitignore vendored
View File

@@ -117,4 +117,6 @@ logs/oAuthClient-log.log
.node-persist/**
/*.env.*
/*.env.*
client/cypress/e2e/[1,2]-*

17
client/cypress.config.js Normal file
View File

@@ -0,0 +1,17 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
experimentalStudio: true,
env: {
FIREBASE_USERNAME: "cypress@imex.test",
FIREBASE_PASSWORD: "cypress",
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "http://localhost:3000",
},
});

4
client/cypress.env.json Normal file
View File

@@ -0,0 +1,4 @@
{
"graphql_dev_endpoint": "https://db.dev.bodyshop.app/v1/graphql",
"uploaded_by_email": "john@imex.dev"
}

View File

@@ -1,8 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -0,0 +1,232 @@
import moment from "moment";
import job from "../../fixtures/jobs/job-3.json";
describe(
"Adding job to checklist",
{
defaultCommandTimeout: 10000,
},
() => {
beforeEach(() => {
cy.visit("/manage/jobs");
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_BODYSHOP") {
req.alias = "bodyshop";
}
});
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
});
it("adds checklists to the job and set the job to production", () => {
const tomorrow = moment(new Date()).format("YYYY-MM-DD");
cy.get('[data-cy="job-actions-button"]').click();
// Go to intake
cy.get('[data-cy="job-intake-button"]').should("not.be.disabled").click();
cy.url().should("include", "/intake");
// Fill out the form
cy.get('[data-cy="checklist-form"]').should("be.visible");
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
// intakechecklist
const checklists = bodyshop.intakechecklist.form;
checklists.forEach((item, index) => {
if (item.type === "text") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:text")
.type("Random Word");
} else if (item.type === "textarea") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("textarea")
.type("Random Word");
} else if (item.type === "checkbox") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:checkbox")
.check();
} else if (item.type === "slider") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-slider-dot:eq(1)")
.click({ force: true });
} else if (item.type === "rate") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-rate > li")
.eq(3)
.find("div[role='radio']")
.click({ force: true });
}
});
});
// Check if `Add Job to Production` is switched to on
cy.get('[data-cy="add-to-production-switch"]').should(
"have.attr",
"aria-checked",
"true"
);
// Select dates for completion and delivery
cy.get("#scheduled_completion").find(".ant-picker-input").first().click();
cy.get(`[title="${tomorrow}"]`).should("be.visible").click();
// Add time selection
cy.get("#scheduled_delivery").find(".ant-picker-input").first().click();
cy.get(`[title="${tomorrow}"]`)
.should("be.visible")
.click({ multiple: true, force: true });
// Add time selection
// Add note
cy.get('[data-cy="checklist-production-note"]').type("automated testing");
// Submit the form
cy.get('[data-cy="checklist-submit-button"]').click();
cy.url().should("include", "/manage/jobs");
cy.contains("In Production");
});
it("adds checklists to the job and remove the job to production", () => {
const tomorrow = moment(new Date()).format("YYYY-MM-DD");
cy.get('[data-cy="job-actions-button"]').click();
// Go to deliver
cy.get('[data-cy="job-deliver"]').should("not.be.disabled").click();
cy.url().should("include", "/deliver");
// Fill out the form
cy.get('[data-cy="checklist-form"]').should("be.visible");
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
// deliverchecklist
const checklists = bodyshop.deliverchecklist.form;
checklists.forEach((item, index) => {
if (item.type === "text") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:text")
.type("Random Word");
} else if (item.type === "textarea") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("textarea")
.type("Random Word");
} else if (item.type === "checkbox") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:checkbox")
.check();
} else if (item.type === "slider") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-slider-dot:eq(1)")
.click({ force: true });
} else if (item.type === "rate") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-rate > li")
.eq(3)
.find("div[role='radio']")
.click({ force: true });
}
});
});
// Select dates for completion and delivery
cy.get("#actual_completion").find(".ant-picker-input").first().click();
cy.get(`[title="${tomorrow}"]`).should("be.visible").click();
cy.get("#actual_delivery").find(".ant-picker-input").first().click();
cy.get(`[title="${tomorrow}"]`)
.should("be.visible")
.click({ multiple: true, force: true });
cy.get('[data-cy="remove-from-production"]').should(
"have.attr",
"aria-checked",
"true"
);
// Submit the form
cy.get('[data-cy="checklist-submit-button"]').click();
// Job checklist completed.
cy.url().should("include", "/manage/jobs");
cy.contains("Delivered");
});
it("renders and check the checklists correctly", () => {
// Click the actions button
cy.get('[data-cy="job-actions-button"]').click();
// Go to checklists
cy.get('[data-cy="job-checklist"]').should("not.be.disabled").click();
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
// intakechecklist
const intakechecklist = bodyshop.intakechecklist.form;
// deliverchecklist
const deliverchecklist = bodyshop.deliverchecklist.form;
const checklists = [...intakechecklist, ...deliverchecklist];
cy.get('[data-cy="intake-checklist"]')
.should("be.visible")
.find("input")
.should("be.disabled");
cy.get('[data-cy="deliver-checklist"]')
.should("be.visible")
.find("input")
.should("be.disabled");
checklists.forEach((item, index) => {
if (item.type === "text") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:text")
.should("have.value", "Random Word");
} else if (item.type === "textarea") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("textarea")
.should("have.value", "Random Word");
} else if (item.type === "checkbox") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find("input:checkbox")
.should("be.checked");
} else if (item.type === "slider") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-slider-handle")
.should("have.attr", "aria-valuenow", item.max / 2);
} else if (item.type === "rate") {
cy.get('[data-cy="config-form-components"] > div')
.eq(index)
.find(".ant-rate > .ant-rate-star-full")
.should("have.length", 3);
}
});
});
});
}
);

View File

@@ -0,0 +1,149 @@
import job from "../../fixtures/jobs/job-3.json";
const errorMessages = {
class: "Class is required. ",
referral_source: "Referral Source is required. ",
employee_csr: "Customer Service Rep. is required. ",
category: "Category is required. ",
};
describe(
"Converting a job with ",
{
defaultCommandTimeout: 10000,
},
() => {
beforeEach(() => {
cy.visit("/manage/jobs");
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_BODYSHOP") {
req.alias = "bodyshop";
}
});
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
cy.get('[data-cy="job-convert-button"]').click();
});
it("shows the error messages of required fields", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get('[data-cy="convert-button"]').click();
cy.get("#ins_co_nm_help")
.find(".ant-form-item-explain-error")
.should("have.text", "Insurance Company Name is required. ");
for (const id in data.config) {
if (data.config[id]) {
cy.get(`#${id}_help`)
.find(".ant-form-item-explain-error")
.should("have.text", errorMessages[id]);
}
}
});
});
it("shows error of required fields when insurance company is selected", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
cy.get("#ins_co_nm_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
cy.get("#driveable").should("have.class", "ant-switch").click();
cy.get("#towin").should("have.class", "ant-switch").click();
cy.get('[data-cy="convert-button"]').click();
for (const id in data.config) {
if (data.config[id]) {
cy.get(`#${id}_help`)
.find(".ant-form-item-explain-error")
.should("have.text", errorMessages[id]);
}
}
});
});
it("checks for the job to convert", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
cy.get("#ins_co_nm_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
for (const id in data.config) {
if (data.config[id]) {
cy.get(".ant-select-selection-search").find(`#${id}`).click();
cy.get(`#${id}_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click();
}
}
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
cy.get("#driveable").should("have.class", "ant-switch").click();
cy.get("#towin").should("have.class", "ant-switch").click();
// cy.get('[data-cy="convert-button"]').click();
// cy.get(".ant-notification-notice-message").contains("successfully");
});
});
}
);

View File

@@ -0,0 +1,500 @@
import job from "../../fixtures/jobs/job-3.json";
import job2 from "../../fixtures/jobs/job-4.json";
import jobSupplement from "../../fixtures/jobs/job-3-supplement.json";
import jobMetadata from "../../fixtures/jobs/job-3-jobmetadata.json";
import jobSupplementMetadata from "../../fixtures/jobs/job-3-supplment-jobmetadata.json";
import Dinero from "dinero.js";
const createJobEstimate = (job, bodyshopid) => {
return {
owner: {
data: {
shopid: bodyshopid,
...job.owner.data,
},
},
vehicle: {
data: {
shopid: bodyshopid,
...job.vehicle.data,
},
},
shopid: bodyshopid,
...job,
};
};
describe(
"Importing an available job",
{
defaultCommandTimeout: 10000,
requestTimeout: 10000,
},
() => {
// assuming that user is logged in
beforeEach(() => {
cy.visit("/manage/available");
// intercept bodyshop query for id
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_BODYSHOP") {
req.alias = "bodyshop";
}
});
cy.wait("@bodyshop").then(({ response }) => {
const id = response.body.data.bodyshops[0].id;
cy.wrap(id).as("bodyshopid");
});
});
it("Enters a job programatically", () => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
req.alias = "availableJobs";
}
});
cy.wait("@availableJobs").then(({ request }) => {
const token = request.headers.authorization;
cy.get("@bodyshopid").then((bodyshopid) => {
const job_est_data = createJobEstimate(job, bodyshopid);
cy.insertAvailableJob({
bodyshopid,
job,
token,
job_est_data,
});
});
});
});
it("creates a new owner record for the job", () => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
req.alias = "availableJobs";
}
});
cy.wait("@availableJobs");
cy.get('[data-cy="available-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.contains(job.clm_no)
.parent()
.as("row");
cy.get("@row")
.find('[data-cy="add-job-as-new-button"]')
.should("be.enabled")
.click();
cy.get('[data-cy="new_owner_checkbox"]').should("be.checked");
cy.get('[data-cy="existing-owners-ok-button"]')
.should("be.enabled")
.click();
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "INSERT_JOB") {
req.alias = "insertJob";
}
});
cy.wait("@insertJob").then(({ response }) => {
const id = response.body.data.insert_jobs.returning[0].id;
cy.get(".ant-notification-notice-message")
.contains("Job created successfully. Click to view.")
.click();
cy.url().should("include", `/manage/jobs/${id}`);
});
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
cy.get('[data-cy="job-totals-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("totals-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@totals-table")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(1)
.invoke("text")
.should(
"be.equal",
Dinero({
amount: jobMetadata.totals.subtotal.amount,
}).toFormat()
);
});
it("imports a supplement for an existing job", () => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
req.alias = "availableJobs";
}
});
cy.wait("@availableJobs").then(({ request }) => {
const token = request.headers.authorization;
cy.get("@bodyshopid").then((bodyshopid) => {
const job_est_data = createJobEstimate(jobSupplement, bodyshopid);
cy.insertAvailableJob({
bodyshopid,
job: jobSupplement,
token,
job_est_data,
});
});
});
cy.get('[data-cy="refetch-available-jobs-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="available-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.contains(jobSupplement.clm_no)
.parent()
.as("row");
cy.get('[data-cy="add-job-as-supplement"]').should("be.enabled").click();
cy.get('[data-cy="existing-jobs-table"]')
.find(".ant-table-tbody tr")
.should("not.have.class", "ant-table-placeholder")
.first()
.click();
cy.get('[data-cy="existing-jobs-ok-button"]')
.should("not.be", "disabled")
.click();
cy.get(".ant-notification-notice-message")
.contains("Job supplemented successfully.")
.click();
cy.url().should("include", "/manage/jobs");
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
cy.get('[data-cy="job-totals-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("totals-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@totals-table")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(1)
.invoke("text")
.should(
"be.equal",
Dinero({
amount: jobSupplementMetadata.totals.subtotal.amount,
}).toFormat()
);
});
it("imports a supplement and override estimate header", () => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
req.alias = "availableJobs";
}
});
cy.wait("@availableJobs").then(({ request }) => {
const token = request.headers.authorization;
cy.get("@bodyshopid").then((bodyshopid) => {
const job_est_data = createJobEstimate(jobSupplement, bodyshopid);
cy.insertAvailableJob({
bodyshopid,
job: jobSupplement,
token,
job_est_data,
});
});
});
cy.get('[data-cy="refetch-available-jobs-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="available-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.contains(jobSupplement.clm_no)
.parent()
.as("row");
cy.get('[data-cy="add-job-as-supplement"]').should("be.enabled").click();
cy.get('[data-cy="existing-jobs-table"]')
.find(".ant-table-tbody tr")
.should("not.have.class", "ant-table-placeholder")
.first()
.click();
// click override
cy.get('[data-cy="override-header-checkbox"]').check();
cy.get('[data-cy="existing-jobs-ok-button"]')
.should("not.be", "disabled")
.click();
cy.get(".ant-notification-notice-message")
.contains("Job supplemented successfully.")
.click();
cy.url().should("include", "/manage/jobs");
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
cy.get('[data-cy="job-totals-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("totals-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@totals-table")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(1)
.invoke("text")
.should(
"be.equal",
Dinero({
amount: jobSupplementMetadata.totals.subtotal.amount,
}).toFormat()
);
});
it("imports a job with an existing owner", () => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
req.alias = "availableJobs";
}
});
cy.wait("@availableJobs").then(({ request }) => {
const token = request.headers.authorization;
cy.get("@bodyshopid").then((bodyshopid) => {
const job_est_data = createJobEstimate(job2, bodyshopid);
cy.insertAvailableJob({
bodyshopid,
job: job2,
token,
job_est_data,
});
});
});
cy.get('[data-cy="refetch-available-jobs-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="add-job-as-new-button"]').should("be.enabled").click();
cy.get('[data-cy="existing-owner-table"]', { timeout: 20000 })
.find(".ant-table-tbody tr")
.should("not.have.class", "ant-table-placeholder")
.then(($table) => {
cy.wrap($table).first().click();
cy.get('[data-cy="new_owner_checkbox"]').should("not.be", "checked");
});
cy.get('[data-cy="existing-owners-ok-button"]').click();
cy.get(".ant-notification-notice-message").contains(
"Job created successfully. Click to view."
);
cy.visit("/manage/owners");
// Navigate to owner records
cy.get('[data-cy="owners-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("owners-table")
.should("not.have.class", "ant-table-placeholder");
// Get owner name
cy.get("@owners-table")
.contains(`${job2.owner.data.ownr_fn} ${job2.owner.data.ownr_ln}`)
.click();
// check list if claim number is there
cy.get('[data-cy="owner-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("owner-jobs-table")
.should("not.have.class", "ant-table-placeholder");
// Get owner name
cy.get("@owner-jobs-table").contains(job2.clm_no).should("exist");
});
}
);
const errorMessages = {
class: "Class is required. ",
referral_source: "Referral Source is required. ",
employee_csr: "Customer Service Rep. is required. ",
category: "Category is required. ",
};
describe(
"Converting an active job",
{
defaultCommandTimeout: 10000,
},
() => {
beforeEach(() => {
cy.visit("/manage/jobs");
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_BODYSHOP") {
req.alias = "bodyshop";
}
});
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
cy.get('[data-cy="job-convert-button"]').click();
});
it("shows the error messages of required fields", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get('[data-cy="convert-button"]').click();
cy.get("#ins_co_nm_help")
.find(".ant-form-item-explain-error")
.should("have.text", "Insurance Company Name is required. ");
for (const id in data.config) {
if (data.config[id]) {
cy.get(`#${id}_help`)
.find(".ant-form-item-explain-error")
.should("have.text", errorMessages[id]);
}
}
});
});
it("shows error of required fields when insurance company is selected", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
cy.get("#ins_co_nm_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
cy.get("#driveable").should("have.class", "ant-switch").click();
cy.get("#towin").should("have.class", "ant-switch").click();
cy.get('[data-cy="convert-button"]').click();
for (const id in data.config) {
if (data.config[id]) {
cy.get(`#${id}_help`)
.find(".ant-form-item-explain-error")
.should("have.text", errorMessages[id]);
}
}
});
});
it("checks for the job to convert", () => {
cy.wait("@bodyshop").then(({ response }) => {
const bodyshop = response.body.data.bodyshops[0];
const data = {
ins_cos: bodyshop.md_ins_cos,
config: {
class: bodyshop.enforce_class,
referral_source: bodyshop.enforce_referral,
employee_csr: bodyshop.enforce_conversion_csr,
category: bodyshop.enforce_conversion_category,
},
};
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
cy.get("#ins_co_nm_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
for (const id in data.config) {
if (data.config[id]) {
cy.get(".ant-select-selection-search").find(`#${id}`).click();
cy.get(`#${id}_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click();
}
}
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
cy.get("#driveable").should("have.class", "ant-switch").click();
cy.get("#towin").should("have.class", "ant-switch").click();
cy.get('[data-cy="convert-button"]').click();
cy.get(".ant-notification-notice-message").contains(
"Job converted successfully."
);
});
});
}
);

View File

@@ -0,0 +1,32 @@
describe("logging in to the application", () => {
// FIXME error message
it("logs in the using wrong credentials", () => {
cy.login("fakeusername", "veryverylongpassword_123@#");
cy.contains("invalid-email");
});
it("logs in the using wrong password", () => {
cy.login("john@imex.dev", "veryverylongpassword_123@#");
cy.contains(
"The email and password combination you provided is incorrect."
);
});
it("logs in a non-existent credentials", () => {
cy.login("franz@imex.dev", "veryverylongpassword_123@#");
cy.contains("A user with this email does not exist.");
});
// TODO create disabled account
// it("logs in with a disabled account", () => {
// cy.login("disabled_account@imex.dev", "john123");
// cy.contains("User account disabled.");
// });
// TODO log in to the application
// it("logs in the using the right credentials", () => {
// cy.login("john@imex.dev", "john123");
// cy.url().should('include', '/manage')
// });
});

View File

@@ -0,0 +1,17 @@
describe("resetting user password", () => {
it("resets forgotten password with an invalid email", () => {
cy.passwordReset("franz");
cy.contains("Email is not a valid email");
});
// FIXME error message
it("resets forgotten password with a user that does not exist", () => {
cy.passwordReset("franz@imex.dev");
cy.contains("user-not-found");
});
it("resets forgotten password using the right credentials", () => {
cy.passwordReset("john@imex.dev");
cy.contains("A password reset link has been sent to you.");
});
});

View File

@@ -0,0 +1,131 @@
import job from "../../fixtures/jobs/job-3.json";
import job2 from "../../fixtures/jobs/job-4.json";
import moment from "moment";
describe(
"Ordering parts for the job",
{
defaultCommandTimeout: 10000,
},
() => {
beforeEach(() => {
cy.visit("/manage/jobs");
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
});
it("order parts for the job", () => {
const today = moment(new Date()).format("YYYY-MM-DD");
cy.get("@active-jobs-table")
.contains(job.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
// Go to repair data tab
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
// Click on filter parts only
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
// Select multiple rows
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-checkbox-input")
.click();
// Click Order Parts
cy.get('[data-cy="order-parts-button"]')
.should("not.be.disabled")
.click();
// Modal should be visible
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
// Fill required fields
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
cy.get(`#vendorid_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
cy.get("#deliver_by").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="part-order-select-none"]').check();
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
cy.get(".ant-notification-notice-message").contains(
"Parts order created successfully."
);
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Ordered");
});
it.only("order multiple parts for the job", () => {
const today = moment(new Date()).format("YYYY-MM-DD");
cy.get("@active-jobs-table")
.contains(job2.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
// Go to repair data tab
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
// Click on filter parts only
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
// Select multiple rows
cy.get('[data-cy="repair-data-table"]')
.find("input:checkbox")
.first()
.click();
// Click Order Parts
cy.get('[data-cy="order-parts-button"]')
.should("not.be.disabled")
.click();
// Modal should be visible
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
// Fill required fields
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
cy.get(`#vendorid_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
cy.get("#deliver_by").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
cy.get('[data-cy="part-order-select-none"]').check();
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
cy.get(".ant-notification-notice-message").contains(
"Parts order created successfully."
);
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Ordered");
});
}
);

View File

@@ -0,0 +1,89 @@
describe(
"Entering payment for the job",
{
defaultCommandTimeout: 5000,
},
() => {
beforeEach(() => {
cy.visit("/manage");
cy.get("body").then(($body) => {
if ($body.text().includes("Login")) {
// Log in
cy.get('[data-cy="username"]').type("john@imex.dev");
cy.get('[data-cy="password"]').type("john123");
cy.get('[data-cy="sign-in-button"]').click();
}
cy.get(".ant-table-tbody")
.should("be.visible")
.find("tr")
.should("not.have.class", "ant-table-placeholder");
cy.get(".ant-table-row")
.not(':contains("Open")')
.first()
.find("a")
.first()
.click();
cy.url().should("include", "/manage/jobs");
// Go to totals data tab
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
});
});
it("enters a payment manually", () => {
cy.get('[data-cy="job-payment-button"]').should("be.visible").click();
// fill out form
cy.get('[data-cy="payment-amount"]').type(100);
cy.get('[data-cy="payment-transactionid"]').type("QBD-P-03");
cy.get('[data-cy="payment-memo"]').type("e2e testing");
cy.get('[data-cy="payment-date"]').click();
cy.get('[title="2023-07-03"]').should("be.visible").click();
cy.antdSelect("payer");
cy.antdSelect("type");
cy.get('[data-cy="payment-form-save"]').click();
});
// TODO Add payment using intellipay
it("marks payment as exported", () => {
cy.get('[data-cy="payments-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("payments-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@payments-table")
.first()
.find('[data-cy="edit-payment-button"]')
.click();
cy.get('[data-cy="payment-markexported"]')
.should("not.be.disabled")
.click();
});
it("marks payment for re-export", () => {
cy.get('[data-cy="payments-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("payments-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@payments-table")
.last()
.find('[data-cy="edit-payment-button"]')
.click();
cy.get('[data-cy="payment-markforreexport"]')
.should("not.be.disabled")
.click();
});
}
);

View File

@@ -0,0 +1,431 @@
import job from "../../fixtures/jobs/job-3.json";
import moment from "moment";
const uuid = () => Cypress._.random(0, 1e6);
describe(
"Billing job parts orders",
{
defaultCommandTimeout: 10000,
},
() => {
const today = moment(new Date()).format("YYYY-MM-DD");
beforeEach(() => {
cy.viewport(1280, 720);
cy.visit("/manage/jobs");
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "SEARCH_VENDOR_AUTOCOMPLETE") {
req.alias = "vendors";
}
});
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
});
it("receives a part bill", () => {
// Order a part
// Go to repair data tab
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
// Click on filter parts only
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
// Select multiple rows
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-checkbox-input")
.click();
// Click Order Parts
cy.get('[data-cy="order-parts-button"]')
.should("not.be.disabled")
.click();
// Modal should be visible
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
// Fill required fields
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
cy.get(`#vendorid_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
cy.get("#deliver_by").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
cy.get('[data-cy="part-order-select-none"]').check();
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
cy.get(".ant-notification-notice-message").contains(
"Parts order created successfully."
);
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Ordered");
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
// Find the first row in the parts order
cy.get('[data-cy="part-orders-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("orders-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@orders-table")
.first()
.should("be.visible")
.find('[data-cy="receive-bill-button"]')
.click();
// fill out form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
cy.get("#location_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
});
// Click save
cy.get('[data-cy="bill-form-save-button"]').click();
cy.get(".ant-notification-notice-message").contains(
"Invoice added successfully."
);
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Received");
});
it("backorders part from order", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="part-orders-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("orders-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@orders-table")
.first()
.should("be.visible")
.find('[data-cy="view-part-order-button"]')
.click();
cy.get('[data-cy="mark-backorder-button"]').click();
cy.get(".backorder-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="mark-for-backorder-button"]').click();
cy.get(".ant-drawer-close").click();
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Backordered");
});
it("order parts inhouse", () => {
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
// Click on filter parts only
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
// Select multiple rows
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-checkbox-input")
.click();
// Click Order Parts
cy.get('[data-cy="order-parts-inhouse-button"]')
.should("not.be.disabled")
.click();
cy.antdSelect("bill-vendor");
cy.antdSelect("bill-cost-center");
cy.get('[data-cy="bill-form-save-button"]').click({ force: true });
cy.get(".ant-notification-notice-message").contains(
"Invoice added successfully."
);
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find(".ant-table-cell")
.eq(15)
.contains("Received");
});
it("check inhouse bill to have extra actions", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("bills-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@bills-table")
.contains("$0.00")
.parent()
.parent()
.first()
.find('[data-cy="print-wrapper"]')
.should("exist");
});
it("posts bill directly", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-post-button"]').should("be.visible").click();
// Add New Line
cy.get('[data-cy="bill-line-add-button"]')
.should("not.be.disabled")
.click();
// Select Vendor
cy.antdSelect("bill-vendor");
// Select Line
cy.antdSelect("bill-line");
cy.get('[data-cy="bill-line-line-desc"]').type("Line Description");
// Fill the Form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
cy.get("#location_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
});
cy.antdSelect("bill-cost-center");
// Click save
cy.get('[data-cy="bill-form-save-button"]').click();
cy.get(".ant-notification-notice-message").contains(
"Invoice added successfully."
);
});
it("posts a bill with save and new", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-post-button"]').should("be.visible").click();
// Add New Line
cy.get('[data-cy="bill-line-add-button"]')
.should("not.be.disabled")
.click();
// Select Vendor
cy.antdSelect("bill-vendor");
// Select Line
cy.antdSelect("bill-line", "-- Not On Estimate --");
// Fill the Form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
cy.get("#location_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
});
cy.antdSelect("bill-cost-center");
// Click save
cy.get('[data-cy="bill-form-savenew-button"]')
.should("not.be.disabled")
.click();
cy.get(".ant-notification-notice-message").contains(
"Invoice added successfully."
);
});
it("uploads a document to a bill", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("bills-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@bills-table")
.first()
.should("be.visible")
.find('[data-cy="edit-bill-button"]')
.click();
cy.location("search").should("include", "billid");
cy.get('[data-cy="bill-edit-form"]')
.find(".ant-upload #bill-document-upload")
.selectFile("job.json", { force: true });
});
it("marks bill as exported", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("bills-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@bills-table")
.find('[data-cy="bill-exported-checkbox"]')
.not(":checked")
.first()
.as("export-status")
.parent()
.parent()
.parent()
.parent()
.find('[data-cy="edit-bill-button"]')
.click();
cy.location("search").should("include", "billid");
cy.get('[data-cy="bill-mark-export-button"]')
.as("mark-for-export")
.click();
cy.get("@mark-for-export").should("be.disabled");
});
it("marks bill for re-export", () => {
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
cy.get('[data-cy="bills-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("bills-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@bills-table")
.find('[data-cy="bill-exported-checkbox"]')
.filter(":checked")
.first()
.as("export-status")
.parent()
.parent()
.parent()
.parent()
.find('[data-cy="edit-bill-button"]')
.click();
cy.location("search").should("include", "billid");
cy.get('[data-cy="bill-mark-reexport-button"]')
.as("mark-for-reexport")
.click();
cy.get("@mark-for-reexport").should("be.disabled");
});
}
);

View File

@@ -0,0 +1,487 @@
import job2 from "../../fixtures/jobs/job-4.json";
import moment from "moment";
import Dinero from "dinero.js";
const uuid = () => Cypress._.random(0, 1e6);
describe(
"Validating and calculating bills",
{
defaultCommandTimeout: 10000,
},
() => {
const today = moment(new Date()).format("YYYY-MM-DD");
const jobLines = job2.joblines.data.filter(
(line) => line.part_type === "PAS" || line.part_type === "PAE"
);
const linesTotal = jobLines.reduce(
(prev, line) => prev + line.act_price,
0
);
beforeEach(() => {
cy.viewport(1280, 720);
cy.visit("/manage/jobs");
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "SEARCH_VENDOR_AUTOCOMPLETE") {
req.alias = "vendors";
}
});
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job2.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
// Go to repair data tab
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
});
it("validates auto reconciliation through posting bill", () => {
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
// Click on filter parts only
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
// Select multiple rows
cy.get('[data-cy="repair-data-table"]')
.find(".ant-checkbox-input")
.first()
.click();
// Click Order Parts
cy.get('[data-cy="order-parts-button"]')
.should("not.be.disabled")
.click();
// Modal should be visible
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
// Fill required fields
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
cy.get(`#vendorid_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
cy.get("#deliver_by").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
cy.get('[data-cy="part-order-select-none"]').check();
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
cy.get(".ant-notification-notice-message").contains(
"Parts order created successfully."
);
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
// Find the first row in the parts order
cy.get('[data-cy="part-orders-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("orders-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@orders-table")
.first()
.should("be.visible")
.find('[data-cy="receive-bill-button"]')
.click();
// fill out form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
cy.get("#location_list")
.next()
.find(".ant-select-item-option-content")
.first()
.click();
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.as("retailPrice")
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
// Get taxes add it to the sum
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
const subtotals = taxes
.toArray()
.map((el) => Number(el.innerText.substring(1)));
const totalTax = Cypress._.sum(subtotals);
const billAmount = sum + totalTax;
cy.get('[data-cy="bill-form-bill-total"]')
.find("input")
.clear()
.type(billAmount);
});
});
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
// Click save
cy.get('[data-cy="bill-form-save-button"]').click();
cy.get("@retailPrice")
.invoke("val")
.then((val) => {
const discrepancy = linesTotal - Number(val);
cy.get("#retailtotal").should("have.text", `$${val}`);
cy.get(".discrepancy").each(($statistic) => {
cy.wrap($statistic).should(
"have.text",
Dinero({
amount: discrepancy,
precision: 0,
}).toFormat()
);
});
});
});
it("returning item and validating statistics", () => {
cy.get('[data-cy="bills-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("bills-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@bills-table")
.find('[data-cy="credit-memo-checkbox"]')
.filter(":not(:checked)")
.first()
.should("not.be.disabled")
.parent()
.parent()
.parent()
.parent()
.find('[data-cy="return-items-button"]')
.click();
cy.get('[data-cy="billline-checkbox"]').check();
cy.get('[data-cy="billline-return-items-ok-button"]').click();
cy.get('[data-cy="billline-actual-price"]')
.find(".ant-form-item-control-input-content")
.then((prices) => {
const totals = prices
.toArray()
.map((el) => Number(el.innerText.substring(1)));
const sum = Cypress._.sum(totals);
const price = Dinero({
amount: sum * 100,
}).toFormat();
cy.get('[data-cy="order-quantity"]').each((input) => {
cy.wrap(input).type("1");
});
cy.get('[data-cy="part-order-select-none"]').click();
cy.get('[data-cy="order-part-submit"]').click();
cy.get("#totalReturns").should("have.text", price);
cy.get("#calculatedcreditsnotreceived").should("have.text", price);
cy.get("#creditsnotreceived").should("have.text", price);
});
});
it("receives credit memo without return part", () => {
// Find the first row in the parts order
cy.get('[data-cy="part-orders-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("orders-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@orders-table")
.find('[data-cy="part-order-return-checkbox"]')
.filter(":checked")
.first()
.parent()
.parent()
.parent()
.parent()
.find('[data-cy="receive-bill-button"]')
.click();
// fill out form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="is-credit-memo-switch"]').click();
cy.get('[data-cy="is-credit-memo-switch"]').should(
"have.attr",
"aria-checked",
"false"
);
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
cy.get('[data-cy="bill-line-actual-price"]').then((priceCells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const priceTotals = priceCells
.toArray()
.map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
const priceSum = Cypress._.sum(priceTotals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
// Get taxes add it to the sum
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
const subtotals = taxes
.toArray()
.map((el) => Number(el.innerText.substring(1)));
const totalTax = Cypress._.sum(subtotals);
const billAmount = sum + totalTax;
cy.get('[data-cy="bill-form-bill-total"]')
.find("input")
.clear()
.type(billAmount);
});
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
cy.get(`.ant-select-bill-cost-center > .ant-select-selector`).each(
(select) => {
cy.wrap(select).click();
cy.wrap(select)
.find("input")
.invoke("attr", "id")
.then((id) => {
cy.get(`#${id}_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
});
}
);
cy.get('[data-cy="bill-form-save-button"]').click();
cy.get("#totalReturns")
.invoke("text")
.then((value) => {
const totalReturns =
Number(value.substring(1)) - priceSum < 0
? 0
: Number(value.substring(1)) - priceSum;
cy.get("#calculatedcreditsnotreceived").should(
"have.text",
Dinero({
amount: totalReturns,
precision: 0,
}).toFormat()
);
});
});
});
});
it("receives credit memo with return part", () => {
// Find the first row in the parts order
cy.get('[data-cy="part-orders-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("orders-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@orders-table")
.find('[data-cy="part-order-return-checkbox"]')
.filter(":checked")
.first()
.parent()
.parent()
.parent()
.parent()
.find('[data-cy="receive-bill-button"]')
.click();
// fill out form
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
cy.get("#bill-form-date").click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.get('[data-cy="bill-line-table"]').each(($row) => {
// get retail amount
cy.wrap($row)
.find('[data-cy="bill-line-actual-price"]')
.click({ force: true, multiple: true });
cy.wrap($row)
.find('[data-cy="bill-line-actual-cost"]')
.click({ multiple: true });
});
cy.get('[data-cy="is-credit-memo-switch"]').should(
"have.attr",
"aria-checked",
"true"
);
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
cy.get('[data-cy="bill-line-actual-price"]').then((priceCells) => {
const totals = cells.toArray().map((el) => Number(el.value));
const priceTotals = priceCells
.toArray()
.map((el) => Number(el.value));
const sum = Cypress._.sum(totals);
const priceSum = Cypress._.sum(priceTotals);
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
// Get taxes add it to the sum
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
const subtotals = taxes
.toArray()
.map((el) => Number(el.innerText.substring(1)));
const totalTax = Cypress._.sum(subtotals);
const billAmount = sum + totalTax;
cy.get('[data-cy="bill-form-bill-total"]')
.find("input")
.clear()
.type(billAmount);
});
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
cy.get(`.ant-select-bill-cost-center > .ant-select-selector`).each(
(select) => {
cy.wrap(select).click();
cy.wrap(select)
.find("input")
.invoke("attr", "id")
.then((id) => {
cy.get(`#${id}_list`)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
});
}
);
cy.get('[data-cy="mark-as-received-checkbox"]').check({
multiple: true,
});
cy.get('[data-cy="bill-form-save-button"]').click();
cy.get("#totalReturns")
.invoke("text")
.then((value) => {
const totalReturns =
Number(value.substring(1)) - priceSum < 0
? 0
: Number(value.substring(1)) - priceSum;
cy.get("#creditsnotreceived").should(
"have.text",
Dinero({
amount: totalReturns,
precision: 0,
}).toFormat()
);
});
});
});
});
it("views the row expander if it has the order and bill", () => {
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
cy.get('[data-cy="filter-parts-button"]')
.should("not.be.disabled")
.click();
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find("td")
.first()
.click();
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find("td")
.eq(13)
.find("div")
.should("exist");
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find("td")
.eq(14)
.find("div")
.should("exist");
cy.get('[data-cy="repair-data-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.first()
.find("td")
.eq(15)
.find("div")
.should("have.text", "Returned");
cy.get('[data-cy="parts-bills-order"]')
.should("be.visible")
.find("li")
.first()
.should("not.have.text", "This part has not yet been ordered.");
});
}
);

View File

@@ -0,0 +1,318 @@
import moment from "moment";
import job2 from "../../fixtures/jobs/job-4.json";
import Dinero from "dinero.js";
const uuid = () => Cypress._.random(0, 1e6);
describe(
"Entering payment for the job",
{
defaultCommandTimeout: 5000,
},
() => {
const today = moment().format("YYYY-MM-DD");
const LABOR_HOURS = 1;
const COST_CENTER = "Body";
beforeEach(() => {
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_ACTIVE_EMPLOYEES") {
req.alias = "employees";
}
});
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
if (req.body.operationName === "QUERY_BODYSHOP") {
req.alias = "bodyshop";
}
});
cy.visit("/manage");
cy.get('[data-cy="active-jobs-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("active-jobs-table")
.should("not.have.class", "ant-table-placeholder");
cy.get("@active-jobs-table")
.contains(job2.clm_no)
.first()
.parent()
.find('[data-cy="active-job-link"]')
.click();
cy.url().should("include", "/manage/jobs");
});
it("checks input validations", () => {
cy.get('[data-cy="job-actions-button"]').click();
cy.get('[data-cy="actions-timetickets"]')
.should("be.visible")
.and("not.be.disabled")
.click();
cy.get('[data-cy="timeticket-save-button"]').first().click();
cy.get('[data-cy="form-timeticket"]')
.find(".ant-form-item-explain-error")
.should("have.length", 4);
cy.get('[data-cy="form-timeticket-date"]').click();
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
cy.antdSelect("timeticket-employee");
cy.antdSelect("cost-center");
cy.get('[data-cy="labor-allocations-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("labor-allocations")
.should("not.have.class", "ant-table-placeholder");
cy.get("@labor-allocations")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(4)
.find("strong")
.invoke("text")
.as("bodyDiff")
.then((diff) => {
// TODO dynamically select the employee prior to what is the labor and cost
cy.get('[data-cy="form-timeticket-productivehrs"]').type(
Number(diff) + 1
);
cy.get(".ant-form-item-explain-error").should(
"have.text",
"The number of hours entered is more than what is available for this cost center."
);
});
});
it("adds new time ticket to a job with flat rate", () => {
cy.get('[data-cy="job-actions-button"]').click();
cy.get('[data-cy="actions-timetickets"]')
.should("be.visible")
.and("not.be.disabled")
.click();
cy.get('[data-cy="labor-allocations-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("labor-allocations")
.should("not.have.class", "ant-table-placeholder");
// Get Difference for Body
cy.get('[data-cy="form-timeticket-date"]').click();
cy.get(`[title="${today}"]`).should("be.visible").click();
cy.get("@labor-allocations")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(4)
.find("strong")
.invoke("text")
.as("bodyDiff")
.then((diff) => {
cy.get('[data-cy="form-timeticket-productivehrs"]').type(LABOR_HOURS);
cy.get('[data-cy="form-timeticket-actualhrs"]').type(LABOR_HOURS);
});
cy.wait("@employees").then(({ response }) => {
const employees = response.body.data.employees;
const employee = employees.find((e) => e.flat_rate);
const employee_name = `${employee.first_name} ${employee.last_name}`;
cy.antdSelectValue("timeticket-employee", employee_name);
cy.antdSelectValue("cost-center", COST_CENTER);
});
cy.get('[data-cy="form-timeticket-memo"]').type(uuid());
cy.get('[data-cy="timeticket-save-button"]').first().click();
cy.get(".ant-notification-notice-message").contains(
"Time ticket entered successfully."
);
});
it("adds new time ticket to a job with straight rate", () => {
cy.get('[data-cy="job-actions-button"]').click();
cy.get('[data-cy="actions-timetickets"]')
.should("be.visible")
.and("not.be.disabled")
.click();
cy.get('[data-cy="labor-allocations-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("labor-allocations")
.should("not.have.class", "ant-table-placeholder");
// Get Difference for Body
cy.get('[data-cy="form-timeticket-date"]').click();
cy.get(`[title="${today}"]`).should("be.visible").click();
cy.get("@labor-allocations")
.eq(0)
.find("td:not(.ant-table-selection-column)")
.eq(4)
.find("strong")
.invoke("text")
.as("bodyDiff")
.then((diff) => {
cy.get('[data-cy="form-timeticket-productivehrs"]').type(LABOR_HOURS);
cy.get('[data-cy="form-timeticket-actualhrs"]').type(LABOR_HOURS);
});
cy.wait("@employees").then(({ response }) => {
const employees = response.body.data.employees;
const employee = employees.find((e) => !e.flat_rate);
const employee_name = `${employee.first_name} ${employee.last_name}`;
cy.antdSelectValue("timeticket-employee", employee_name);
cy.antdSelectValue("cost-center", COST_CENTER);
});
cy.get('[data-cy="form-timeticket-memo"]').type(uuid());
cy.get('[data-cy="timeticket-save-button"]').first().click();
cy.get(".ant-notification-notice-message").contains(
"Time ticket entered successfully."
);
});
it("checks hours calculated to the allocations table", () => {
cy.get('[data-cy="tab-labor"]').should("be.visible").click();
cy.get('[data-cy="labor-allocations-table"]')
.find(".ant-table-tbody")
.find("> tr:not(.ant-table-measure-row)")
.as("labor-allocations")
.should("not.have.class", "ant-table-placeholder");
cy.get('[data-cy="labor-total-hrs-claimed"]')
.invoke("text")
.then((hours) => {
cy.wrap(hours).should("not.equal", "0");
});
});
it("checks the job costing calculations", () => {
cy.get('[data-cy="job-actions-button"]').click();
cy.get('[data-cy="actions-jobcosting"]')
.should("be.visible")
.and("not.be.disabled")
.click();
cy.wait("@bodyshop").then(({ request }) => {
const token = request.headers.authorization;
const query = `query QUERY_ACTIVE_EMPLOYEES {
employees(where: { active: { _eq: true } }) {
last_name
id
first_name
employee_number
active
termination_date
hire_date
flat_rate
rates
pin
user_email
}
}`;
cy.request({
url: "http://localhost:4000/test/query",
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: token,
},
body: {
query,
},
}).then((response) => {
const cost_center = COST_CENTER;
const employees = response.body.employees;
const total_cost = employees.reduce((prev, employee) => {
const rate =
employee.rates.find((rate) => rate.cost_center === cost_center)
.rate ?? 0;
return prev + rate * LABOR_HOURS;
}, 0);
cy.get('[data-cy="responsibilitycenter"]')
.contains(cost_center)
.parent()
.parent()
.find('[data-cy="cost"]')
.should(
"have.text",
Dinero({
amount: total_cost,
precision: 0,
}).toFormat()
);
});
});
});
it.only("clocks in and out of the tech page for the timeticket", () => {
// TODO go to tech page for the clock in and out
cy.visit("/tech");
cy.get('[data-cy="tech-employee-id"]').type("a");
cy.get('[data-cy="tech-employee-password"]').type("a{enter}");
cy.contains("Logged in as");
// go to clock in
cy.get('[data-cy="sider-joblock"]').click({ force: true });
// find the job ro
cy.get('[data-cy="clock-ro-select"]').type("273");
cy.get('[data-cy="clock-ro-select"] .ant-select-selection-search input')
.invoke("attr", "id")
.then((selElm) => {
const dropDownSelector = `#${selElm}_list`;
cy.get(dropDownSelector)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
});
// select cost center
cy.get('[data-cy="clock-cost-center-select"]').click();
cy.get(
'[data-cy="clock-cost-center-select"] .ant-select-selection-search input'
)
.invoke("attr", "id")
.then((selElm) => {
const dropDownSelector = `#${selElm}_list`;
cy.get(dropDownSelector)
.next()
.find(".ant-select-item-option-content")
.contains("Body")
.first()
.click({ force: true });
});
// clock in and out of the job
});
}
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
{
"parts": {
"parts": {
"list": {
"PAE": {
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"PAN": {
"total": {
"amount": 26661,
"currency": "USD",
"precision": 2
}
}
},
"total": {
"amount": 26661,
"currency": "USD",
"precision": 2
},
"subtotal": {
"amount": 26661,
"currency": "USD",
"precision": 2
},
"prt_dsmk_total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"sublets": {
"total": {
"amount": 5000,
"currency": "USD",
"precision": 2
},
"subtotal": {
"amount": 5000,
"currency": "USD",
"precision": 2
}
}
},
"rates": {
"la1": {
"rate": 92.49,
"hours": 3.5,
"total": {
"amount": 32372,
"currency": "USD",
"precision": 2
}
},
"la2": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"la3": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"la4": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"laa": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lab": {
"rate": 85.16,
"hours": 29.7,
"total": {
"amount": 252925,
"currency": "USD",
"precision": 2
}
},
"lad": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lae": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"laf": {
"rate": 97.34,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lag": {
"rate": 85.16,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lam": {
"rate": 109.5,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lar": {
"rate": 85.16,
"hours": 8.500000000000002,
"total": {
"amount": 72386,
"currency": "USD",
"precision": 2
}
},
"las": {
"rate": 85.16,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lau": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"mapa": {
"rate": 55.38,
"hours": 8.500000000000002,
"total": {
"amount": 47073,
"currency": "USD",
"precision": 2
}
},
"mash": {
"rate": 6.85,
"hours": 33.199999999999996,
"total": {
"amount": 22742,
"currency": "USD",
"precision": 2
}
},
"subtotal": {
"amount": 427498,
"currency": "USD",
"precision": 2
},
"rates_subtotal": {
"amount": 357683,
"currency": "USD",
"precision": 2
}
},
"totals": {
"subtotal": {
"amount": 495355,
"currency": "USD",
"precision": 2
},
"local_tax": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"state_tax": {
"amount": 34675,
"currency": "USD",
"precision": 2
},
"custPayable": {
"total": {
"amount": 30000,
"currency": "USD",
"precision": 2
},
"dep_taxes": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"deductible": {
"amount": 30000,
"currency": "USD",
"precision": 2
},
"federal_tax": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"other_customer_amount": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"federal_tax": {
"amount": 24768,
"currency": "USD",
"precision": 2
},
"net_repairs": {
"amount": 524798,
"currency": "USD",
"precision": 2
},
"statePartsTax": {
"amount": 2216,
"currency": "USD",
"precision": 2
},
"total_repairs": {
"amount": 554798,
"currency": "USD",
"precision": 2
}
},
"additional": {
"pvrt": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"total": {
"amount": 36196,
"currency": "USD",
"precision": 2
},
"towing": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"storage": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"shipping": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"adjustments": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"additionalCosts": {
"amount": 36196,
"currency": "USD",
"precision": 2
},
"additionalCostItems": [
{
"key": "ATS Amount",
"total": {
"amount": 36196,
"currency": "USD",
"precision": 2
}
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
{
"parts": {
"parts": {
"list": {
"PAE": {
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"PAN": {
"total": {
"amount": 36661,
"currency": "USD",
"precision": 2
}
}
},
"total": {
"amount": 36661,
"currency": "USD",
"precision": 2
},
"subtotal": {
"amount": 36661,
"currency": "USD",
"precision": 2
},
"prt_dsmk_total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"sublets": {
"total": {
"amount": 5000,
"currency": "USD",
"precision": 2
},
"subtotal": {
"amount": 5000,
"currency": "USD",
"precision": 2
}
}
},
"rates": {
"la1": {
"rate": 92.49,
"hours": 3.5,
"total": {
"amount": 32372,
"currency": "USD",
"precision": 2
}
},
"la2": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"la3": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"la4": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"laa": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lab": {
"rate": 85.16,
"hours": 29.7,
"total": {
"amount": 252925,
"currency": "USD",
"precision": 2
}
},
"lad": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lae": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"laf": {
"rate": 97.34,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lag": {
"rate": 85.16,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lam": {
"rate": 109.5,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lar": {
"rate": 85.16,
"hours": 8.500000000000002,
"total": {
"amount": 72386,
"currency": "USD",
"precision": 2
}
},
"las": {
"rate": 85.16,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"lau": {
"rate": 0,
"hours": 0,
"total": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"mapa": {
"rate": 55.38,
"hours": 8.500000000000002,
"total": {
"amount": 47073,
"currency": "USD",
"precision": 2
}
},
"mash": {
"rate": 6.85,
"hours": 33.199999999999996,
"total": {
"amount": 22742,
"currency": "USD",
"precision": 2
}
},
"subtotal": {
"amount": 427498,
"currency": "USD",
"precision": 2
},
"rates_subtotal": {
"amount": 357683,
"currency": "USD",
"precision": 2
}
},
"totals": {
"subtotal": {
"amount": 505355,
"currency": "USD",
"precision": 2
},
"local_tax": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"state_tax": {
"amount": 35375,
"currency": "USD",
"precision": 2
},
"custPayable": {
"total": {
"amount": 30000,
"currency": "USD",
"precision": 2
},
"dep_taxes": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"deductible": {
"amount": 30000,
"currency": "USD",
"precision": 2
},
"federal_tax": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"other_customer_amount": {
"amount": 0,
"currency": "USD",
"precision": 2
}
},
"federal_tax": {
"amount": 25268,
"currency": "USD",
"precision": 2
},
"net_repairs": {
"amount": 535998,
"currency": "USD",
"precision": 2
},
"statePartsTax": {
"amount": 2916,
"currency": "USD",
"precision": 2
},
"total_repairs": {
"amount": 565998,
"currency": "USD",
"precision": 2
}
},
"additional": {
"pvrt": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"total": {
"amount": 36196,
"currency": "USD",
"precision": 2
},
"towing": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"storage": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"shipping": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"adjustments": {
"amount": 0,
"currency": "USD",
"precision": 2
},
"additionalCosts": {
"amount": 36196,
"currency": "USD",
"precision": 2
},
"additionalCostItems": [
{
"key": "ATS Amount",
"total": {
"amount": 36196,
"currency": "USD",
"precision": 2
}
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
{
"id": 8739,
"name": "Jane",
"email": "jane@example.com"
}

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,23 +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 ==== */
});
});

View File

@@ -1,143 +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')
})
})
})

View File

@@ -1,299 +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 })
})
})

View File

@@ -1,39 +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)
})
})

View File

@@ -1,177 +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-elements-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)
})
})
})
})

View File

@@ -1,97 +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)
})
})
})
})

View File

@@ -1,77 +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')
})
})

View File

@@ -1,202 +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'])
})
})

View File

@@ -1,88 +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')
})
})
})

View File

@@ -1,52 +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')
})
})
})

View File

@@ -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')
})
})

View File

@@ -1,104 +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')
})
})

View File

@@ -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
},
})
})
})

View File

@@ -1,163 +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)
})
})

View File

@@ -1,114 +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()
})
})
})

View File

@@ -1,205 +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))
})
})

View File

@@ -1,121 +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)
})
})

View File

@@ -1,110 +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
})
})
})
})

View File

@@ -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)
})
})

View File

@@ -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])
})
})

View File

@@ -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')
})
})

View File

@@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js 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
}

View File

@@ -24,4 +24,104 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import "@testing-library/cypress/add-commands";
Cypress.Commands.add("goToSignInPage", () => {
cy.visit("/");
cy.contains("Sign In").click();
});
Cypress.Commands.add("login", (username, password) => {
cy.goToSignInPage();
cy.get('[data-cy="username"]').type(username);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="sign-in-button"]', { timeout: 2000 }).click();
});
Cypress.Commands.add("passwordReset", (email) => {
cy.goToSignInPage();
cy.get('[data-cy="reset-password"]').click();
cy.get('[data-cy="reset-password-email-input"]').type(email);
cy.get('[data-cy="reset-password-button"]').click();
});
Cypress.on("uncaught:exception", (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false;
});
Cypress.Commands.add("antdSelect", (selector, filter) => {
cy.get(`.ant-select-${selector} > .ant-select-selector`).click();
cy.get(`.ant-select-${selector} .ant-select-selection-search input`)
.invoke("attr", "id")
.then((selElm) => {
const dropDownSelector = `#${selElm}_list`;
if (filter) {
cy.get(dropDownSelector)
.next()
.find(".ant-select-item-option-content")
.not(`:contains("${filter}")`)
.first()
.click({ force: true });
} else {
cy.get(dropDownSelector)
.next()
.find(".ant-select-item-option-content")
.first()
.click({ force: true });
}
});
});
Cypress.Commands.add("antdSelectValue", (selector, filter) => {
cy.get(`.ant-select-${selector} > .ant-select-selector`).click();
cy.get(`.ant-select-${selector} .ant-select-selection-search input`)
.invoke("attr", "id")
.then((selElm) => {
const dropDownSelector = `#${selElm}_list`;
cy.get(dropDownSelector)
.next()
.find(".ant-select-item-option-content")
.contains(filter)
.first()
.click({ force: true });
});
});
Cypress.Commands.add(
"insertAvailableJob",
({ bodyshopid, job, job_est_data, token }) => {
const query = `mutation INSERT_AVAILABLE_JOB($job: available_jobs_insert_input!) {
insert_available_jobs_one(object: $job) {
id
}
}`;
cy.request({
url: "http://localhost:4000/test/query",
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: token,
},
body: {
query,
job: {
est_data: job_est_data,
uploaded_by: Cypress.env("uploaded_by_email"),
cieca_id: job.ciecaid,
bodyshopid,
clm_amt: job.clm_total,
clm_no: job.clm_no,
ins_co_nm: job.ins_co_nm,
ownr_name: `${job.owner.data.ownr_fn} ${job.owner.data.ownr_ln}`,
vehicle_info: `${job.v_model_yr} ${job.v_make_desc} ${job.v_model_desc}`,
},
},
})
.its("body.insert_available_jobs_one")
.should("have.property", "id");
}
);

View File

@@ -1,5 +1,5 @@
// ***********************************************************
// This example support/index.js is processed and
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
@@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": ["cypress"]
},
"include": ["**/*.*"]
}

5912
client/job.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@
"socket.io-client": "^4.6.1",
"styled-components": "^5.3.6",
"subscriptions-transport-ws": "^0.11.0",
"uniqid": "^5.4.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.3",
@@ -121,7 +122,7 @@
"devDependencies": {
"@sentry/webpack-plugin": "^1.20.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.1",
"cypress": "^12.13.0",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",

View File

@@ -117,7 +117,7 @@ export default function BillCmdReturnsTableComponent({
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox />
<Checkbox data-cy="mark-as-received-checkbox" />
</Form.Item>
</td>
</tr>

View File

@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -194,12 +194,19 @@ export function BillDetailEditcontainer({
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
<BillReeportButtonComponent
data-cy="bill-mark-reexport-button"
bill={data && data.bills_by_pk}
/>
<BillMarkExportedButton
data-cy="bill-mark-export-button"
bill={data && data.bills_by_pk}
/>
</Space>
}
/>
<Form
data-cy="bill-edit-form"
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}

View File

@@ -77,11 +77,14 @@ export function BillDetailEditReturn({
return (
<>
<Modal
visible={visible}
open={visible}
onCancel={() => setVisible(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
okButtonProps={{
"data-cy": "billline-return-items-ok-button",
}}
>
<Form
initialValues={data && data.bills_by_pk}
@@ -96,6 +99,7 @@ export function BillDetailEditReturn({
<tr>
<td>
<Checkbox
data-cy="billline-checkbox"
onChange={(e) => {
form.setFieldsValue({
billlines: form
@@ -150,6 +154,7 @@ export function BillDetailEditReturn({
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
data-cy="billline-actual-price"
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
@@ -173,6 +178,7 @@ export function BillDetailEditReturn({
</Form>
</Modal>
<Button
data-cy="return-items-button"
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setVisible(true);

View File

@@ -362,11 +362,16 @@ function BillEnterModalContainer({
{t("bills.labels.generatepartslabel")}
</Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
<Button
data-cy="bill-form-save-button"
loading={loading}
onClick={() => form.submit()}
>
{t("general.actions.save")}
</Button>
{billEnterModal.context && billEnterModal.context.id ? null : (
<Button
data-cy="bill-form-savenew-button"
type="primary"
loading={loading}
onClick={() => {

View File

@@ -177,6 +177,7 @@ export function BillFormComponent({
]}
>
<VendorSearchSelect
className="ant-select-bill-vendor"
disabled={disabled}
options={vendorAutoCompleteOptions}
onSelect={handleVendorSelect}
@@ -249,7 +250,10 @@ export function BillFormComponent({
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
<Input
data-cy="bill-form-invoice"
disabled={disabled || disableInvNumber}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
@@ -261,7 +265,7 @@ export function BillFormComponent({
},
]}
>
<FormDatePicker disabled={disabled} />
<FormDatePicker id="bill-form-date" disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
@@ -300,9 +304,10 @@ export function BillFormComponent({
}),
]}
>
<Switch />
<Switch data-cy="is-credit-memo-switch" />
</Form.Item>
<Form.Item
data-cy="bill-form-bill-total"
label={t("bills.fields.total")}
name="total"
rules={[
@@ -316,7 +321,12 @@ export function BillFormComponent({
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
<Select
data-cy="bill-form-parts-bin"
style={{ width: "10rem" }}
disabled={disabled}
allowClear
>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
@@ -375,17 +385,29 @@ export function BillFormComponent({
/>
<Statistic
title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()}
valueRender={() => (
<span data-cy="bill-form-tax">
{totals.federalTax.toFormat()}
</span>
)}
precision={2}
/>
<Statistic
title={t("bills.labels.state_tax")}
value={totals.stateTax.toFormat()}
valueRender={() => (
<span data-cy="bill-form-tax">
{totals.stateTax.toFormat()}
</span>
)}
precision={2}
/>
<Statistic
title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()}
valueRender={() => (
<span data-cy="bill-form-tax">
{totals.localTax.toFormat()}
</span>
)}
precision={2}
/>
<Statistic
@@ -406,7 +428,12 @@ export function BillFormComponent({
? "green"
: "red",
}}
value={totals.discrepancy.toFormat()}
// value={totals.discrepancy.toFormat()}
valueRender={() => (
<span id="bill-form-discrepancy">
{totals.discrepancy.toFormat()}
</span>
)}
precision={2}
/>
</Space>
@@ -456,19 +483,20 @@ export function BillFormComponent({
}}
>
<Upload.Dragger
id="bill-image-upload"
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<div>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</div>
</Upload.Dragger>
</Form.Item>
</div>

View File

@@ -1,14 +1,15 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button, Form,
Button,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip
Tooltip,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -79,6 +80,7 @@ export function BillEnterModalLinesComponent({
),
formInput: (record, index) => (
<BillLineSearchSelect
className="ant-select-bill-line"
disabled={disabled}
options={lineData}
style={{ width: "100%", minWidth: "10rem" }}
@@ -132,7 +134,9 @@ export function BillEnterModalLinesComponent({
],
};
},
formInput: (record, index) => <Input disabled={disabled} />,
formInput: (record, index) => (
<Input data-cy="bill-line-line-desc" disabled={disabled} />
),
},
{
title: t("billlines.fields.quantity"),
@@ -194,6 +198,7 @@ export function BillEnterModalLinesComponent({
},
formInput: (record, index) => (
<CurrencyInput
data-cy="bill-line-actual-price"
min={0}
disabled={disabled}
onBlur={(e) => {
@@ -241,6 +246,7 @@ export function BillEnterModalLinesComponent({
},
formInput: (record, index) => (
<CurrencyInput
data-cy="bill-line-actual-cost"
min={0}
disabled={disabled}
controls={false}
@@ -313,7 +319,12 @@ export function BillEnterModalLinesComponent({
};
},
formInput: (record, index) => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
<Select
showSearch
style={{ minWidth: "3rem" }}
disabled={disabled}
className="ant-select-bill-cost-center"
>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => (
@@ -572,6 +583,7 @@ export function BillEnterModalLinesComponent({
return (
<>
<Table
data-cy="bill-line-table"
components={{
body: {
cell: EditableCell,
@@ -587,6 +599,7 @@ export function BillEnterModalLinesComponent({
/>
<Form.Item>
<Button
data-cy="bill-line-add-button"
disabled={disabled}
onClick={() => {
add();

View File

@@ -32,6 +32,7 @@ export function BillMarkExportedButton({
bodyshop,
authLevel,
bill,
...props
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -92,7 +93,12 @@ export function BillMarkExportedButton({
if (hasAccess)
return (
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
<Button
loading={loading}
disabled={bill.exported}
onClick={handleUpdate}
{...props}
>
{t("bills.labels.markexported")}
</Button>
);

View File

@@ -24,7 +24,12 @@ export default connect(
mapDispatchToProps
)(BillMarkForReexportButton);
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
export function BillMarkForReexportButton({
bodyshop,
authLevel,
bill,
...props
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -73,6 +78,7 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
loading={loading}
disabled={!bill.exported}
onClick={handleUpdate}
{...props}
>
{t("bills.labels.markforreexport")}
</Button>

View File

@@ -54,7 +54,10 @@ export function BillsListTableComponent({
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<Button
onClick={() => handleOnRowClick(record)}
data-cy="edit-bill-button"
>
<EditFilled />
</Button>
)}
@@ -126,7 +129,12 @@ export function BillsListTableComponent({
sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo} />,
render: (text, record) => (
<Checkbox
data-cy="credit-memo-checkbox"
checked={record.is_credit_memo}
/>
),
},
{
title: t("bills.fields.exported"),
@@ -135,7 +143,9 @@ export function BillsListTableComponent({
sorter: (a, b) => a.exported - b.exported,
sortOrder:
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported} />,
render: (text, record) => (
<Checkbox data-cy="bill-exported-checkbox" checked={record.exported} />
),
},
{
title: t("general.labels.actions"),
@@ -178,6 +188,7 @@ export function BillsListTableComponent({
{job && job.converted ? (
<>
<Button
data-cy="bills-post-button"
onClick={() => {
setBillEnterContext({
actions: { refetch: billsQuery.refetch },
@@ -217,6 +228,7 @@ export function BillsListTableComponent({
}
>
<Table
data-cy="bills-table"
loading={billsQuery.loading}
scroll={{
x: true, // y: "50rem"

View File

@@ -16,7 +16,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
},
]}
>
<Checkbox disabled={readOnly} />
<Checkbox data-cy="checklist-item-checkbox" disabled={readOnly} />
</Form.Item>
);
}

View File

@@ -3,7 +3,7 @@ import FormTypes from "./config-form-types";
export default function ConfirmFormComponents({ componentList, readOnly }) {
return (
<div>
<div data-cy="config-form-components">
{componentList.map((f, idx) => {
const Comp = FormTypes[f.type];

View File

@@ -4,6 +4,18 @@ import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required, min, max } = formItem;
const marks = {
[min]: {
label: <span style={{ height: 0 }}>&nbsp;</span>,
},
[max / 2]: {
label: <span style={{ height: 0 }}>&nbsp;</span>,
},
[max]: {
label: <span style={{ height: 0 }}>&nbsp;</span>,
},
};
return (
<Form.Item
name={name}
@@ -15,7 +27,13 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
},
]}
>
<Slider disabled={readOnly} min={min || 0} max={max || 10} />
<Slider
disabled={readOnly}
min={min || 0}
max={max || 10}
marks={marks}
style={{ marginBottom: 0, marginTop: 0 }}
/>
</Form.Item>
);
}

View File

@@ -35,6 +35,7 @@ export function DocumentsLocalUploadComponent({
return (
<Upload.Dragger
id="bill-document-upload"
multiple={true}
fileList={fileList}
onChange={(f) => {

View File

@@ -5,13 +5,11 @@ const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
if (!value) return null;
switch (type) {
case "text":
return <div>{value}</div>;
return <>{value}</>;
case "currency":
return (
<div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>
);
return <>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</>;
default:
return <div>{value}</div>;
return <>{value}</>;
}
};
export default forwardRef(ReadOnlyFormItem);

View File

@@ -106,6 +106,7 @@ function Header({
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
subMenuCloseDelay={0.3}
data-cy="header-menu"
>
<Menu.Item key="home" icon={<HomeFilled />}>
<Link to="/manage">{t("menus.header.home")}</Link>
@@ -347,8 +348,14 @@ function Header({
currentUser.email ||
t("general.labels.unknown")
}
data-cy="user-sub-menu"
>
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
<Menu.Item
key="signout"
danger
data-cy="sign-out-button"
onClick={() => signOutStart()}
>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item

View File

@@ -84,6 +84,8 @@ export default function JobBillsTotalComponent({
})
);
console.log(totals.parts.parts.total);
const totalPartsSublet = Dinero(totals.parts.parts.total)
.add(Dinero(totals.parts.sublets.total))
.add(Dinero(totals.additional.shipping))
@@ -127,7 +129,10 @@ export default function JobBillsTotalComponent({
>
<Statistic
title={t("bills.labels.retailtotal")}
value={billTotals.toFormat()}
// value={billTotals.toFormat()}
valueRender={() => (
<span id="retailtotal">{billTotals.toFormat()}</span>
)}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>
@@ -145,7 +150,12 @@ export default function JobBillsTotalComponent({
valueStyle={{
color: discrepancy.getAmount() === 0 ? "green" : "red",
}}
value={discrepancy.toFormat()}
// value={discrepancy.toFormat()}
valueRender={() => (
<span id="discrepancy" className="discrepancy">
{discrepancy.toFormat()}
</span>
)}
/>
</Tooltip>
<Typography.Title>+</Typography.Title>
@@ -178,7 +188,12 @@ export default function JobBillsTotalComponent({
valueStyle={{
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithLbrAdj.toFormat()}
// value={discrepWithLbrAdj.toFormat()}
valueRender={() => (
<span id="discrepWithLbrAdj" className="discrepancy">
{discrepWithLbrAdj.toFormat()}
</span>
)}
/>
</Tooltip>
<Typography.Title>+</Typography.Title>
@@ -193,7 +208,10 @@ export default function JobBillsTotalComponent({
>
<Statistic
title={t("bills.labels.totalreturns")}
value={totalReturns.toFormat()}
// value={totalReturns.toFormat()}
valueRender={() => (
<span id="totalReturns">{totalReturns.toFormat()}</span>
)}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>
@@ -211,7 +229,12 @@ export default function JobBillsTotalComponent({
valueStyle={{
color: discrepWithCms.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithCms.toFormat()}
// value={discrepWithCms.toFormat()}
valueRender={() => (
<span id="discrepWithCms" className="discrepancy">
{discrepWithCms.toFormat()}
</span>
)}
/>
</Tooltip>
</Space>
@@ -231,7 +254,10 @@ export default function JobBillsTotalComponent({
>
<Statistic
title={t("bills.labels.totalreturns")}
value={totalReturns.toFormat()}
// value={totalReturns.toFormat()}
valueRender={() => (
<span id="totalReturns">{totalReturns.toFormat()}</span>
)}
/>
</Tooltip>
<Tooltip
@@ -253,11 +279,18 @@ export default function JobBillsTotalComponent({
? "green"
: "red",
}}
value={
calculatedCreditsNotReceived.getAmount() >= 0
? calculatedCreditsNotReceived.toFormat()
: Dinero().toFormat()
}
// value={
// calculatedCreditsNotReceived.getAmount() >= 0
// ? calculatedCreditsNotReceived.toFormat()
// : Dinero().toFormat()
// }
valueRender={() => (
<span id="calculatedcreditsnotreceived">
{calculatedCreditsNotReceived.getAmount() >= 0
? calculatedCreditsNotReceived.toFormat()
: Dinero().toFormat()}
</span>
)}
/>
</Tooltip>
<Tooltip
@@ -277,11 +310,18 @@ export default function JobBillsTotalComponent({
? "green"
: "red",
}}
value={
totalReturnsMarkedNotReceived.getAmount() >= 0
? totalReturnsMarkedNotReceived.toFormat()
: Dinero().toFormat()
}
// value={
// totalReturnsMarkedNotReceived.getAmount() >= 0
// ? totalReturnsMarkedNotReceived.toFormat()
// : Dinero().toFormat()
// }
valueRender={() => (
<span id="creditsnotreceived">
{totalReturnsMarkedNotReceived.getAmount() >= 0
? totalReturnsMarkedNotReceived.toFormat()
: Dinero().toFormat()}
</span>
)}
/>
</Tooltip>
</Space>

View File

@@ -199,6 +199,7 @@ export function JobChecklistForm({
extra={
!readOnly && (
<Button
data-cy="checklist-submit-button"
loading={loading}
type="primary"
onClick={() => form.submit()}
@@ -209,6 +210,7 @@ export function JobChecklistForm({
}
>
<Form
data-cy="checklist-form"
form={form}
onFinish={handleFinish}
initialValues={{
@@ -256,7 +258,7 @@ export function JobChecklistForm({
label={t("checklist.labels.addtoproduction")}
disabled={readOnly}
>
<Switch disabled={readOnly} />
<Switch data-cy="add-to-production-switch" disabled={readOnly} />
</Form.Item>
<Form.Item
name="allow_text_message"
@@ -292,7 +294,11 @@ export function JobChecklistForm({
disabled={readOnly}
trigger="onChange"
>
<Input.TextArea rows={3} disabled={readOnly} />
<Input.TextArea
data-cy="checklist-production-note"
rows={3}
disabled={readOnly}
/>
</Form.Item>
</div>
)}
@@ -324,7 +330,11 @@ export function JobChecklistForm({
label={t("checklist.labels.removefromproduction")}
disabled={readOnly}
>
<Switch disabled={readOnly} defaultChecked={true} />
<Switch
data-cy="remove-from-production"
disabled={readOnly}
defaultChecked={true}
/>
</Form.Item>
</div>
)}

View File

@@ -23,6 +23,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
render: (record) => <span data-cy="responsibilitycenter">{record}</span>,
},
{
title: t("jobs.labels.sales"),
@@ -42,6 +43,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
render: (record) => <span data-cy="cost">{record}</span>,
},
{

View File

@@ -22,12 +22,12 @@ export default function JobLinesExpander({ jobline, jobid }) {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<Row>
<Row data-cy="parts-expanded-row">
<Col md={24} lg={12}>
<Typography.Title level={4}>
{t("parts_orders.labels.parts_orders")}
</Typography.Title>
<Timeline>
<Timeline data-cy="parts-bills-order">
{data.parts_order_lines.length > 0 ? (
data.parts_order_lines.map((line) => (
<Timeline.Item key={line.id}>
@@ -43,7 +43,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Timeline.Item>
))
) : (
<Timeline.Item>
<Timeline.Item data-cy="parts-empty-order">
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}
@@ -51,7 +51,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col md={24} lg={12}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Timeline>
<Timeline data-cy="parts-bills-order">
{data.billlines.length > 0 ? (
data.billlines.map((line) => (
<Timeline.Item key={line.id}>
@@ -71,7 +71,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
@@ -83,7 +83,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Timeline.Item>
))
) : (
<Timeline.Item>
<Timeline.Item data-cy="parts-empty-order">
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}

View File

@@ -441,6 +441,7 @@ export function JobLinesComponent({
</Tag>
)}
<Button
data-cy="order-parts-inhouse-button"
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
@@ -497,6 +498,7 @@ export function JobLinesComponent({
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
data-cy="order-parts-button"
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
@@ -526,6 +528,7 @@ export function JobLinesComponent({
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
data-cy="filter-parts-button"
onClick={() => {
setState((state) => ({
...state,
@@ -575,6 +578,7 @@ export function JobLinesComponent({
}
/>
<Table
data-cy="repair-data-table"
columns={columns}
rowKey="id"
loading={loading}

View File

@@ -100,6 +100,7 @@ export function JobPayments({
<Space wrap>
<Button
// disabled={record.exportedat}
data-cy="edit-payment-button"
onClick={() => {
setPaymentContext({
actions: { refetch: refetch },
@@ -150,6 +151,7 @@ export function JobPayments({
extra={
<Space wrap>
<Button
data-cy="job-payment-button"
disabled={!job.converted}
onClick={() =>
setPaymentContext({
@@ -170,6 +172,7 @@ export function JobPayments({
}
>
<Table
data-cy="payments-table"
columns={columns}
rowKey="id"
pagination={false}

View File

@@ -108,6 +108,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
return (
<Table
data-cy="job-totals-table"
columns={columns}
rowKey="key"
showHeader={false}

View File

@@ -171,12 +171,16 @@ export function JobsAvailableComponent({
{!isClosed && (
<>
<Button
data-cy="add-job-as-new-button"
onClick={() => addJobAsNew(record)}
disabled={record.issupplement}
>
<PlusCircleFilled />
</Button>
<Button onClick={() => addJobAsSupp(record)}>
<Button
data-cy="add-job-as-supplement"
onClick={() => addJobAsSupp(record)}
>
<DownloadOutlined />
</Button>
</>
@@ -214,6 +218,7 @@ export function JobsAvailableComponent({
extra={
<Space wrap>
<Button
data-cy="refetch-available-jobs-button"
onClick={() => {
refetch();
}}
@@ -255,6 +260,7 @@ export function JobsAvailableComponent({
columns={columns}
rowKey="id"
dataSource={availableJobs}
data-cy="available-jobs-table"
onChange={handleTableChange}
/>
</Card>

View File

@@ -3,7 +3,7 @@ import {
useApolloClient,
useLazyQuery,
useMutation,
useQuery
useQuery,
} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd";
@@ -20,7 +20,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import {
DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_JOBS,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK,
} from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
@@ -28,7 +28,7 @@ import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser
selectCurrentUser,
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";

View File

@@ -108,7 +108,7 @@ export function JobsConvertButton({
},
]}
>
<Select>
<Select className="ant-select-ins_co_nm">
{bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}
@@ -127,7 +127,7 @@ export function JobsConvertButton({
},
]}
>
<Select>
<Select className="ant-select-class">
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
@@ -148,7 +148,7 @@ export function JobsConvertButton({
},
]}
>
<Select>
<Select className="ant-select-referral_source">
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
@@ -157,6 +157,7 @@ export function JobsConvertButton({
</Select>
</Form.Item>
<Form.Item
data-cy="referral_source_extra"
label={t("jobs.fields.referral_source_extra")}
name="referral_source_extra"
>
@@ -176,6 +177,7 @@ export function JobsConvertButton({
]}
>
<Select
className="ant-select-employee_csr"
showSearch
style={{ width: 200 }}
optionFilterProp="children"
@@ -210,7 +212,7 @@ export function JobsConvertButton({
},
]}
>
<Select allowClear>
<Select allowClear className="ant-select-category">
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
@@ -243,7 +245,12 @@ export function JobsConvertButton({
<Switch />
</Form.Item>
<Space wrap>
<Button type="danger" onClick={() => form.submit()} loading={loading}>
<Button
type="danger"
data-cy="convert-button"
onClick={() => form.submit()}
loading={loading}
>
{t("jobs.actions.convert")}
</Button>
<Button onClick={() => setVisible(false)}>
@@ -264,6 +271,7 @@ export function JobsConvertButton({
// style={{ display: job.converted ? "none" : "" }}
disabled={job.converted || jobRO}
loading={loading}
data-cy="job-convert-button"
onClick={() => {
setVisible(true);
form.setFieldsValue({

View File

@@ -196,6 +196,7 @@ export function JobsDetailHeaderActions({
</Popover>
</Menu.Item>
<Menu.Item
data-cy="job-intake-button"
disabled={
!!job.intakechecklist ||
!jobInPreProduction ||
@@ -214,7 +215,7 @@ export function JobsDetailHeaderActions({
</Link>
)}
</Menu.Item>
<Menu.Item disabled={!jobInProduction || jobRO}>
<Menu.Item data-cy="job-deliver" disabled={!jobInProduction || jobRO}>
{!jobInProduction ? (
t("jobs.actions.deliver")
) : (
@@ -223,7 +224,7 @@ export function JobsDetailHeaderActions({
</Link>
)}
</Menu.Item>
<Menu.Item disabled={!job.converted}>
<Menu.Item data-cy="job-checklist" disabled={!job.converted}>
<Link to={`/manage/jobs/${job.id}/checklist`}>
{t("jobs.actions.viewchecklist")}
</Link>
@@ -243,7 +244,9 @@ export function JobsDetailHeaderActions({
});
}}
>
{t("timetickets.actions.enter")}
<span data-cy="actions-timetickets">
{t("timetickets.actions.enter")}
</span>
</Menu.Item>
<Menu.Item
key="enterpayments"
@@ -427,7 +430,7 @@ export function JobsDetailHeaderActions({
});
}}
>
{t("jobs.labels.jobcosting")}
<span data-cy="actions-jobcosting">{t("jobs.labels.jobcosting")}</span>
</Menu.Item>
{job && !job.converted && (
<Menu.Item>
@@ -517,7 +520,7 @@ export function JobsDetailHeaderActions({
);
return (
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button>
<Button data-cy="job-actions-button">
<span>{t("general.labels.actions")}</span>
<DownCircleFilled />

View File

@@ -5,7 +5,6 @@ import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.contai
import BillsListTable from "../bills-list-table/bills-list-table.component";
import JobBillsTotal from "../job-bills-total/job-bills-total.component";
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
export default function JobsDetailPliComponent({
job,
@@ -15,7 +14,6 @@ export default function JobsDetailPliComponent({
}) {
return (
<div>
<PartsOrderModal />
{billsQuery.error ? (
<AlertComponent message={billsQuery.error.message} type="error" />
) : null}

View File

@@ -167,6 +167,7 @@ export default function JobsFindModalComponent({
/>
</div>
)}
data-cy="existing-jobs-table"
pagination={{ position: "bottom" }}
columns={columns}
rowKey="id"
@@ -189,6 +190,7 @@ export default function JobsFindModalComponent({
/>
<Divider />
<Checkbox
data-cy="override-header-checkbox"
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
setImportOptions({

View File

@@ -77,7 +77,10 @@ export default connect(
title={t("jobs.labels.existing_jobs")}
width={"80%"}
destroyOnClose
okButtonProps={{ disabled: selectedJob ? false : true }}
okButtonProps={{
disabled: selectedJob ? false : true,
"data-cy": "existing-jobs-ok-button",
}}
{...modalProps}
>
{loading ? <LoadingSpinner /> : null}

View File

@@ -122,6 +122,7 @@ export function JobsList({ bodyshop }) {
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
data-cy="active-job-link"
>
<Space>
{record.ro_number || t("general.labels.na")}
@@ -364,6 +365,7 @@ export function JobsList({ bodyshop }) {
}
>
<Table
data-cy="active-jobs-table"
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}

View File

@@ -206,6 +206,7 @@ export function LaborAllocationsTable({
<Col span={24}>
<Card title={t("jobs.labels.laborallocations")}>
<Table
data-cy="labor-allocations-table"
columns={columns}
rowKey={(record) => `${record.cost_center} ${record.mod_lbr_ty}`}
pagination={false}
@@ -225,7 +226,9 @@ export function LaborAllocationsTable({
{summary.hrs_total.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.hrs_claimed.toFixed(1)}
<span data-cy="labor-total-hrs-claimed">
{summary.hrs_claimed.toFixed(1)}
</span>
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.adjustments.toFixed(1)}

View File

@@ -10,7 +10,7 @@ export default function LoadingSpinner({ loading = true, message, ...props }) {
size="large"
style={{
position: "relative",
alignContent: "center"
alignContent: "center",
}}
delay={200}
tip={message ? message : null}

View File

@@ -75,6 +75,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
}
>
<Table
data-cy="owner-jobs-table"
pagination={{ position: "bottom" }}
columns={columns}
scroll={{ x: true }}

View File

@@ -82,6 +82,7 @@ export default function OwnerFindModalComponent({
return (
<div>
<Table
data-cy="existing-owner-table"
pagination={{ position: "bottom" }}
columns={columns}
rowKey="id"
@@ -106,6 +107,7 @@ export default function OwnerFindModalComponent({
<Checkbox
checked={selectedOwner ? false : true}
onClick={() => setSelectedOwner(null)}
data-cy="new_owner_checkbox"
>
{t("owners.labels.create_new")}
</Checkbox>

View File

@@ -42,6 +42,8 @@ export default function OwnerFindModalContainer({
<Modal
title={t("owners.labels.existing_owners")}
width={"80%"}
okButtonProps={{ "data-cy": "existing-owners-ok-button" }}
cancelButtonProps={{ "data-cy": "existing-owners-cancel-button" }}
{...modalProps}
>
{loading ? <LoadingSpinner /> : null}

View File

@@ -115,6 +115,7 @@ export default function OwnersListComponent({
}
>
<Table
data-cy="owners-table"
loading={loading}
pagination={{
position: "top",

View File

@@ -78,9 +78,13 @@ export function PartsOrderLineBackorderButton({
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<FormDatePicker />
<FormDatePicker className="backorder-date" />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
<Button
data-cy="mark-for-backorder-button"
type="primary"
onClick={() => form.submit()}
>
{t("parts_orders.actions.backordered")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
@@ -95,7 +99,11 @@ export function PartsOrderLineBackorderButton({
visible={visibility}
disabled={disabled}
>
<Button loading={loading} onClick={handlePopover}>
<Button
data-cy="mark-backorder-button"
loading={loading}
onClick={handlePopover}
>
{isAlreadyBackordered
? t("parts_orders.actions.receive")
: t("parts_orders.actions.backordered")}

View File

@@ -90,7 +90,10 @@ export function PartsOrderListTableComponent({
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<Button
data-cy="view-part-order-button"
onClick={() => handleOnRowClick(record)}
>
<EyeFilled />
</Button>
)}
@@ -148,6 +151,7 @@ export function PartsOrderListTableComponent({
</Button>
</Popconfirm>
<Button
data-cy="receive-bill-button"
disabled={
(jobRO ? !record.return : jobRO) ||
record.vendor.id === bodyshop.inhousevendorid
@@ -245,7 +249,12 @@ export function PartsOrderListTableComponent({
sorter: (a, b) => a.return - b.return,
sortOrder:
state.sortedInfo.columnKey === "return" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.return} />,
render: (text, record) => (
<Checkbox
data-cy="part-order-return-checkbox"
checked={record.return}
/>
),
},
{
title: t("parts_orders.fields.deliver_by"),
@@ -478,6 +487,7 @@ export function PartsOrderListTableComponent({
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
<Table
data-cy="part-orders-table"
loading={billsQuery.loading}
scroll={{
x: true,

View File

@@ -74,7 +74,7 @@ export function PartsOrderModalComponent({
);
return (
<div>
<div data-cy="parts-order-modal">
<Form.Item name="returnfrombill" style={{ display: "none" }}>
<Input />
</Form.Item>
@@ -98,6 +98,7 @@ export function PartsOrderModalComponent({
</Form.Item>
<Form.Item
name="deliver_by"
id="deliver_by"
rules={[
{
required: true,
@@ -122,7 +123,7 @@ export function PartsOrderModalComponent({
label={t("parts_orders.labels.removefrompartsqueue")}
valuePropName="checked"
>
<Checkbox />
<Checkbox data-cy="part-order-checkbox-remove" />
</Form.Item>
)}
{OEConnection.treatment === "on" && !isReturn && (
@@ -131,7 +132,7 @@ export function PartsOrderModalComponent({
label={t("parts_orders.labels.is_quote")}
valuePropName="checked"
>
<Checkbox />
<Checkbox data-cy="part-order-checkbox-quote" />
</Form.Item>
)}
@@ -255,7 +256,7 @@ export function PartsOrderModalComponent({
},
]}
>
<InputNumber />
<InputNumber data-cy="order-quantity" />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}
@@ -320,7 +321,7 @@ export function PartsOrderModalComponent({
</Space>
}
>
<Input.TextArea rows={3} />
<Input.TextArea rows={3} data-cy="part-order-comments" />
</Form.Item>
<Form.Item noStyle shouldUpdate>
@@ -333,7 +334,11 @@ export function PartsOrderModalComponent({
value={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio disabled={is_quote} value={"none"}>
<Radio
data-cy="part-order-select-none"
disabled={is_quote}
value={"none"}
>
{t("general.labels.none")}
</Radio>
<Radio disabled={is_quote} value={"e"}>

View File

@@ -358,11 +358,11 @@ export function PartsOrderModalContainer({
}
onCancel={() => toggleModalVisible()}
onOk={() => form.submit()}
okButtonProps={{ loading: saving }}
okButtonProps={{ loading: saving, "data-cy": "order-part-submit" }}
cancelButtonProps={{ loading: saving }}
destroyOnClose
width="75%"
forceRender
// forceRender
>
{error ? <AlertComponent message={error.message} type="error" /> : null}
<Form

View File

@@ -15,11 +15,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PaymentFormComponent({
form,
bodyshop,
disabled,
}) {
export function PaymentFormComponent({ form, bodyshop, disabled }) {
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
@@ -65,16 +61,16 @@ export function PaymentFormComponent({
},
]}
>
<CurrencyInput disabled={disabled} />
<CurrencyInput data-cy="payment-amount" disabled={disabled} />
</Form.Item>
<Form.Item
label={t("payments.fields.transactionid")}
name="transactionid"
>
<Input disabled={disabled} />
<Input data-cy="payment-transactionid" disabled={disabled} />
</Form.Item>
<Form.Item label={t("payments.fields.memo")} name="memo">
<Input disabled={disabled} />
<Input data-cy="payment-memo" disabled={disabled} />
</Form.Item>
<Form.Item
label={t("payments.fields.date")}
@@ -86,7 +82,7 @@ export function PaymentFormComponent({
},
]}
>
<DatePickerFormItem disabled={disabled} />
<DatePickerFormItem data-cy="payment-date" disabled={disabled} />
</Form.Item>
</LayoutFormRow>
@@ -101,7 +97,11 @@ export function PaymentFormComponent({
},
]}
>
<Select disabled={disabled}>
<Select
data-cy="payment-customer"
className="ant-select-payer"
disabled={disabled}
>
<Select.Option value={t("payments.labels.customer")}>
{t("payments.labels.customer")}
</Select.Option>
@@ -133,7 +133,11 @@ export function PaymentFormComponent({
},
]}
>
<Select disabled={disabled}>
<Select
data-cy="payment-type"
className="ant-select-type"
disabled={disabled}
>
{bodyshop.md_payment_types.map((v, idx) => (
<Select.Option key={idx} value={v}>
{v}

View File

@@ -88,6 +88,7 @@ const PaymentMarkForExportButton = ({
onClick={handleClick}
loading={exportLogLoading || updatePaymentLoading}
disabled={!!payment.exportedat}
data-cy="payment-markexported"
>
{t("payments.labels.markexported")}
</Button>

View File

@@ -161,7 +161,11 @@ function PaymentModalContainer({
footer={
<span>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
<Button
data-cy="payment-form-save"
loading={loading}
onClick={() => form.submit()}
>
{t("general.actions.save")}
</Button>
{paymentModal.context && paymentModal.context.id ? null : (

View File

@@ -57,6 +57,7 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
onClick={handleClick}
loading={loading}
disabled={!payment.exportedat}
data-cy="payment-markforreexport"
>
{t("payments.labels.markforreexport")}
</Button>

View File

@@ -18,7 +18,7 @@ export default function PrintWrapperComponent({
};
return (
<Space>
<Space data-cy="print-wrapper">
{children || null}
{!emailOnly && <PrinterFilled onClick={() => handlePrint("p")} />}
<MailFilled onClick={() => handlePrint("e")} />

View File

@@ -69,6 +69,7 @@ export function SignInComponent({
<Input
prefix={<UserOutlined />}
placeholder={t("general.labels.username")}
data-cy="username"
/>
</Form.Item>
<Form.Item
@@ -84,6 +85,7 @@ export function SignInComponent({
prefix={<LockOutlined />}
type="password"
placeholder={t("general.labels.password")}
data-cy="password"
/>
</Form.Item>
{signInError ? (
@@ -97,11 +99,12 @@ export function SignInComponent({
type="primary"
htmlType="submit"
loading={loginLoading}
data-cy="sign-in-button"
>
{t("general.actions.login")}
</Button>
</Form>
<Link to={"/resetpassword"}>
<Link to={"/resetpassword"} data-cy="reset-password">
<Button>{t("general.actions.resetpassword")}</Button>
</Link>
</div>

View File

@@ -34,6 +34,7 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
notExported={!bodyshop.tt_allow_post_to_invoiced}
notInvoiced={!bodyshop.tt_allow_post_to_invoiced}
data-cy="clock-ro-select"
/>
</Form.Item>
@@ -47,7 +48,7 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
},
]}
>
<Select>
<Select data-cy="clock-cost-center-select">
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center} value={item.cost_center}>

View File

@@ -53,7 +53,11 @@ export function TechLogin({
},
]}
>
<Input size="large" autoComplete="new-password" />
<Input
data-cy="tech-employee-id"
size="large"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label={t("tech.fields.pin")}
@@ -65,7 +69,11 @@ export function TechLogin({
},
]}
>
<Input.Password size="large" autoComplete="new-password" />
<Input.Password
data-cy="tech-employee-password"
size="large"
autoComplete="new-password"
/>
</Form.Item>
<Button htmlType="submit" loading={loginLoading} className="login-btn">
{t("general.actions.login")}

View File

@@ -56,7 +56,9 @@ export function TechSider({ technician, techLogout }) {
disabled={!!!technician}
icon={<Icon component={FaBusinessTime} />}
>
<Link to={`/tech/jobclock`}>{t("menus.tech.jobclockin")}</Link>
<Link data-cy="sider-joblock" to={`/tech/jobclock`}>
{t("menus.tech.jobclockin")}
</Link>
</Menu.Item>
<Menu.Item
key="4"

View File

@@ -113,7 +113,7 @@ export function TimeTicketModalComponent({
},
]}
>
<FormDatePicker />
<FormDatePicker data-cy="form-timeticket-date" />
</Form.Item>
<Form.Item
name="employeeid"
@@ -135,6 +135,8 @@ export function TimeTicketModalComponent({
form.setFieldsValue({ flat_rate: emps && emps.flat_rate });
}}
data-cy="form-timeticket-employee"
className="ant-select-timeticket-employee"
/>
</Form.Item>
<Form.Item
@@ -158,7 +160,11 @@ export function TimeTicketModalComponent({
},
]}
>
<CostCenterSelect emps={emps} />
<CostCenterSelect
emps={emps}
data-cy="form-timeticket-cost-center"
className="ant-select-cost-center"
/>
</Form.Item>
);
}}
@@ -235,7 +241,10 @@ export function TimeTicketModalComponent({
},
]}
>
<InputNumber precision={1} />
<InputNumber
data-cy="form-timeticket-productivehrs"
precision={1}
/>
</Form.Item>
)}
</Form.Item>
@@ -262,7 +271,11 @@ export function TimeTicketModalComponent({
}),
]}
>
<InputNumber min={0} precision={1} />
<InputNumber
data-cy="form-timeticket-actualhrs"
min={0}
precision={1}
/>
</Form.Item>
{
<>
@@ -320,7 +333,7 @@ export function TimeTicketModalComponent({
}
<Form.Item label={t("timetickets.fields.memo")} name="memo">
<MemoInput />
<MemoInput data-cy="form-timeticket-memo" />
</Form.Item>
<Form.Item shouldUpdate>
{() => (

Some files were not shown because too many files have changed in this diff Show More