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, ... }:
flake-utils.lib.eachDefaultSystem (system:
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
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs-18_x
nodePackages.vue-cli
nodePackages.vls # vue
nodePackages.typescript-language-server # typescript
nodePackages.vscode-langservers-extracted # html, css, json, eslint
];
devShells = rec {
code = pkgs.mkShell {
packages = with pkgs; [
nodejs-18_x
nodePackages.vue-cli
nodePackages.vls # vue
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/test-utils": "^2.2.6",
"@vue/tsconfig": "^0.1.3",
"cypress": "^12.0.2",
"cypress": "^12.3.0",
"eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
import { useLoader } from "@/shared/lib/composables/loader";
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 type { ListsSelectApiPort } from "./ports";
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 listsStore = useListsStore();
@ -25,9 +26,9 @@ export function useListsSelectStore() {
listsStore.setLists(lists);
}
return reactive({
return {
loading: loader.loading,
selectItems,
fetchMany,
});
}
};
});

View File

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

View File

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

View File

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

View File

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

View File

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