Compare commits

...

5 Commits

22 changed files with 171 additions and 62 deletions

View File

@ -0,0 +1,41 @@
// https://docs.cypress.io/api/introduction/api.html
import { Page } from "../support/pages";
describe("Contacts", () => {
beforeEach(() => {
cy.visit(Page.Contacts);
});
it("should open contacts page", () => {
cy.contains("h1", "Контакты");
});
it("should load contacts table", () => {
cy.get("[cy-test=contacts-table-col-email]")
.should("have.length.gte", 20)
.first()
.should("have.text", "brendon.cremin91@gmail.com");
});
it("should load lists select", () => {
cy.get("[cy-test=lists-select]")
.should("have.value", "")
.within(() => cy.get("option").should("have.length.gte", 20));
});
it("should load contacts of selected list", () => {
cy.get("[cy-test=lists-select]")
.should("have.value", "")
.select("c407a7bd-7930-43bc-8241-2aafd058ed39");
cy.get("[cy-test=contacts-table-col-email]")
.first()
.should("have.text", "camryn90@yahoo.com");
});
it("should open create contacts page", () => {
cy.get("[cy-test=openCreateContactForm-button]").click();
cy.location("pathname").should("eq", Page.CreateContact);
});
});

View File

@ -0,0 +1,20 @@
import { Page } from "../../support/pages";
describe("Create contact", () => {
beforeEach(() => {
cy.visit(Page.CreateContact);
});
it("should create a contact", () => {
const email = "foo@example.com";
cy.get("[cy-test=createContact-form-field-email]").type(email);
cy.get("[cy-test=createContact-button]").click();
cy.location("pathname").should("eq", Page.Contacts);
cy.get("[cy-test=contacts-table-col-email]")
.first()
.should("have.text", email);
});
});

View File

@ -0,0 +1,8 @@
import { Page } from "../support/pages";
describe("Dashboard", () => {
it("should redirect to contacts page", () => {
cy.visit(Page.Dashboard);
cy.location("pathname").should("eq", Page.Contacts);
});
});

View File

@ -1,8 +0,0 @@
// https://docs.cypress.io/api/introduction/api.html
describe("My First Test", () => {
it("visits the app root url", () => {
cy.visit("/");
cy.contains("h1", "You did it!");
});
});

5
cypress/support/pages.ts Normal file
View File

@ -0,0 +1,5 @@
export enum Page {
Dashboard = "/",
Contacts = "/contacts",
CreateContact = "/contacts/new",
}

View File

@ -7,17 +7,42 @@
outputs = { self, nixpkgs, flake-utils, ... }: outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; cypressOverlay = final: prev: {
cypress = prev.cypress.overrideAttrs (oldAttrs: rec {
version = "12.3.0";
src = prev.fetchzip {
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
sha256 = "sha256-RhPH/MBF8lqXeFEm2sd73Z55jgcl45VsmRWtAhckrP0=";
};
});
};
pkgs = import nixpkgs {
inherit system;
overlays = [ cypressOverlay ];
};
in in
{ {
devShells.default = pkgs.mkShell { devShells = rec {
packages = with pkgs; [ code = pkgs.mkShell {
nodejs-18_x packages = with pkgs; [
nodePackages.vue-cli nodejs-18_x
nodePackages.vls # vue nodePackages.vue-cli
nodePackages.typescript-language-server # typescript nodePackages.vls # vue
nodePackages.vscode-langservers-extracted # html, css, json, eslint nodePackages.typescript-language-server # typescript
]; nodePackages.vscode-langservers-extracted # html, css, json, eslint
];
};
e2e = pkgs.mkShell {
shellHook = ''
export CYPRESS_INSTALL_BINARY=0;
export CYPRESS_RUN_BINARY="${pkgs.cypress}/bin/Cypress";
'';
};
default = pkgs.mkShell {
inputsFrom = [ code e2e ];
};
}; };
}); });
} }

2
package-lock.json generated
View File

@ -22,7 +22,7 @@
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6", "@vue/test-utils": "^2.2.6",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"cypress": "^12.0.2", "cypress": "^12.3.0",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",

View File

@ -28,7 +28,7 @@
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6", "@vue/test-utils": "^2.2.6",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"cypress": "^12.0.2", "cypress": "^12.3.0",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import TheHeader from "@/app/common/components/TheHeader.vue"; import TheHeader from "@/app/misc/TheHeader.vue";
</script> </script>
<template> <template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineEmits } from "vue"; import { ref } from "vue";
import { ContactFormApiCreateProps } from "./ports"; import { ContactFormApiCreateProps } from "./ports";
import { useContactFormStore } from "./store"; import { useContactFormStore } from "./store";
@ -22,9 +22,13 @@ async function submitForm() {
<div> <div>
<div> <div>
<label>Email</label> <label>Email</label>
<input v-model="contactData.email" /> <input
cy-test="createContact-formField-email"
v-model="contactData.email"
/>
</div> </div>
<BaseButton <BaseButton
cy-test="createContact"
:isPrimary="true" :isPrimary="true"
:isLoading="store.loading" :isLoading="store.loading"
@click="submitForm" @click="submitForm"

View File

@ -1,13 +1,13 @@
import { useLoader } from "@/shared/lib/composables/loader"; import { useLoader } from "@/shared/lib/composables/loader";
import { defineInjectKey, validInject } from "@/shared/lib/di"; import { defineInjectKey, validInject } from "@/shared/lib/di";
import { reactive } from "vue"; import { defineStore } from "pinia";
import { useContactsStore } from "../store"; import { useContactsStore } from "../store";
import type { ContactFormApiCreateProps, ContactFormApiPort } from "./ports"; import type { ContactFormApiCreateProps, ContactFormApiPort } from "./ports";
export const CONTACT_FORM_API_PROVIDE_KEY = export const CONTACT_FORM_API_PROVIDE_KEY =
defineInjectKey<ContactFormApiPort>("ContactFormApi"); defineInjectKey<ContactFormApiPort>("ContactFormApi");
export function useContactFormStore() { export const useContactFormStore = defineStore("ContactForm", () => {
const api = validInject(CONTACT_FORM_API_PROVIDE_KEY); const api = validInject(CONTACT_FORM_API_PROVIDE_KEY);
const contactsStore = useContactsStore(); const contactsStore = useContactsStore();
@ -18,8 +18,8 @@ export function useContactFormStore() {
contactsStore.addContact(contact); contactsStore.addContact(contact);
} }
return reactive({ return {
loading: loader.loading, loading: loader.loading,
create, create,
}); };
} });

View File

@ -28,6 +28,7 @@ watch(
<template> <template>
<BaseTable <BaseTable
cy-test="contacts"
:loading="store.loading" :loading="store.loading"
:columns="columns" :columns="columns"
:dataItems="store.contacts" :dataItems="store.contacts"

View File

@ -1,6 +1,6 @@
import { useLoader } from "@/shared/lib/composables/loader"; import { useLoader } from "@/shared/lib/composables/loader";
import { defineInjectKey, validInject } from "@/shared/lib/di"; import { defineInjectKey, validInject } from "@/shared/lib/di";
import { storeToRefs } from "pinia"; import { defineStore, storeToRefs } from "pinia";
import { reactive } from "vue"; import { reactive } from "vue";
import { useContactsStore } from "../store"; import { useContactsStore } from "../store";
import type { import type {
@ -11,7 +11,7 @@ import type {
export const CONTACTS_TABLE_API_PROVIDE_KEY = export const CONTACTS_TABLE_API_PROVIDE_KEY =
defineInjectKey<ContactsTableApiPort>("ContactsTableApi"); defineInjectKey<ContactsTableApiPort>("ContactsTableApi");
export function useContactsTableStore() { export const useContactsTableStore = defineStore("ContactsTable", () => {
const api = validInject(CONTACTS_TABLE_API_PROVIDE_KEY); const api = validInject(CONTACTS_TABLE_API_PROVIDE_KEY);
const contactsStore = useContactsStore(); const contactsStore = useContactsStore();
@ -23,9 +23,9 @@ export function useContactsTableStore() {
contactsStore.setContacts(contacts); contactsStore.setContacts(contacts);
} }
return reactive({ return {
loading: loader.loading, loading: loader.loading,
contacts, contacts,
fetchMany, fetchMany,
}); };
} });

View File

@ -32,5 +32,5 @@ store.fetchMany();
</script> </script>
<template> <template>
<BaseSelect v-model="value" :items="selectListItems" /> <BaseSelect cy-test="lists" v-model="value" :items="selectListItems" />
</template> </template>

View File

@ -1,13 +1,14 @@
import { useLoader } from "@/shared/lib/composables/loader"; import { useLoader } from "@/shared/lib/composables/loader";
import { defineInjectKey, validInject } from "@/shared/lib/di"; import { defineInjectKey, validInject } from "@/shared/lib/di";
import { computed, reactive } from "vue"; import { defineStore } from "pinia";
import { computed } from "vue";
import { useListsStore } from "../store"; import { useListsStore } from "../store";
import type { ListsSelectApiPort } from "./ports"; import type { ListsSelectApiPort } from "./ports";
export const LISTS_SELECT_API_PROVIDE_KEY = export const LISTS_SELECT_API_PROVIDE_KEY =
defineInjectKey<ListsSelectApiPort>("ListsTableApi"); defineInjectKey<ListsSelectApiPort>("ListsSelectApi");
export function useListsSelectStore() { export const useListsSelectStore = defineStore("ListsSelect", () => {
const api = validInject(LISTS_SELECT_API_PROVIDE_KEY); const api = validInject(LISTS_SELECT_API_PROVIDE_KEY);
const listsStore = useListsStore(); const listsStore = useListsStore();
@ -25,9 +26,9 @@ export function useListsSelectStore() {
listsStore.setLists(lists); listsStore.setLists(lists);
} }
return reactive({ return {
loading: loader.loading, loading: loader.loading,
selectItems, selectItems,
fetchMany, fetchMany,
}); };
} });

View File

@ -52,7 +52,9 @@ provide(LISTS_SELECT_API_PROVIDE_KEY, listApi);
<BasePageHeader> <BasePageHeader>
Контакты Контакты
<template #extra> <template #extra>
<BaseButton @click="openAddContactForm">Добавить контакт</BaseButton> <BaseButton cy-test="openCreateContactForm" @click="openAddContactForm">
Добавить контакт
</BaseButton>
</template> </template>
</BasePageHeader> </BasePageHeader>
<AudienceListsSelect <AudienceListsSelect

View File

@ -7,7 +7,7 @@ const router = createRouter({
{ {
path: "/", path: "/",
name: "dashboard", name: "dashboard",
redirect: { name: "audience" }, redirect: { name: "audience_contacts" },
}, },
...audienceRoutes, ...audienceRoutes,
{ {

View File

@ -1,14 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps({
isPrimary: boolean; cyTest: { type: String, default: "base" },
isLoading: boolean; isPrimary: Boolean,
}>(); isLoading: Boolean,
});
</script> </script>
<template> <template>
<button <button
type="button" type="button"
:class="{ '--primary': isPrimary, '--loading': isLoading }" :class="{ '--primary': isPrimary, '--loading': isLoading }"
:cy-test="`${cyTest}-button`"
> >
<slot /> <slot />
</button> </button>

View File

@ -8,6 +8,7 @@ interface SelectItem {
} }
const props = defineProps({ const props = defineProps({
cyTest: { type: String, default: "base" },
items: { items: {
type: Array as PropType<SelectItem[]>, type: Array as PropType<SelectItem[]>,
required: true, required: true,
@ -29,7 +30,7 @@ const value = computed({
</script> </script>
<template> <template>
<select v-model="value"> <select v-model="value" :cy-test="`${cyTest}-select`">
<option <option
v-for="selectItem in items" v-for="selectItem in items"
:key="selectItem.value" :key="selectItem.value"

View File

@ -1,29 +1,28 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from "vue"; import { PropType } from "vue";
interface TableColumn { interface TableColumn {
key: string; key: string;
title: string; title: string;
} }
export default defineComponent({ defineProps({
props: { cyTest: { type: String, default: "base" },
loading: Boolean, loading: Boolean,
columns: { columns: {
type: Array as PropType<TableColumn[]>, type: Array as PropType<TableColumn[]>,
required: true, required: true,
}, },
dataItems: { dataItems: {
type: Array, type: Array,
required: true, required: true,
},
}, },
}); });
</script> </script>
<template> <template>
<div class="wrapper" :class="{ '--loading': loading }"> <div class="wrapper" :class="{ '--loading': loading }">
<table> <table :cy-test="`${cyTest}-table`">
<thead> <thead>
<tr> <tr>
<th v-for="column in columns" :key="column.key"> <th v-for="column in columns" :key="column.key">
@ -32,8 +31,16 @@ export default defineComponent({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(dataItem, index) in dataItems" :key="dataItem.id || index"> <tr
<td v-for="column in columns" :key="column.key"> v-for="(dataItem, index) in dataItems"
:key="dataItem.id || index"
:cy-test="`${cyTest}-table-row`"
>
<td
v-for="column in columns"
:key="column.key"
:cy-test="`${cyTest}-table-col-${column.key}`"
>
<slot :name="column.key" :dataItem="dataItem">{{ <slot :name="column.key" :dataItem="dataItem">{{
dataItem[column.key] dataItem[column.key]
}}</slot> }}</slot>