Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dressca のフロントエンドについて Maia v1.0 の変更を移植 #2269

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { HttpResponse, http } from 'msw';
import type { GetLoginUserResponse } from '@/generated/api-client';
import { HttpStatusCode } from 'axios';
import { Roles } from '@/shared/constants/roles';

const user: GetLoginUserResponse = {
userName: '[email protected]',
roles: ['Admin'],
roles: [Roles.ADMIN],
};

export const usersHandlers = [
Expand Down
53 changes: 27 additions & 26 deletions samples/Dressca/dressca-frontend/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,54 @@
"openapi-client:generate": "openapi-generator-cli generate -g typescript-axios -i ./../../dressca-backend/src/Dressca.Web.Admin/dressca-admin-api.json --additional-properties=withSeparateModelsAndApi=true,modelPackage=models,apiPackage=api,supportsES6=true -o ./src/generated/api-client"
},
"dependencies": {
"@heroicons/vue": "^2.1.5",
"axios": "^1.7.7",
"msw": "^2.7.0",
"@vee-validate/yup": "^4.13.2",
"pinia": "^2.2.5",
"vee-validate": "^4.14.4",
"vitest": "^2.1.4",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"yup": "^1.4.0"
"@heroicons/vue": "^2.2.0",
"@vee-validate/yup": "^4.15.0",
"axios": "^1.7.9",
"msw": "2.7.0",
"pinia": "^2.3.0",
"vee-validate": "^4.15.0",
"vitest": "^2.1.8",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"yup": "^1.6.1"
},
"msw": {
"workerDirectory": [
"public"
]
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.15.0",
"@rushstack/eslint-patch": "^1.10.4",
"@openapitools/openapi-generator-cli": "^2.15.3",
"@pinia/testing": "^0.1.7",
"@rushstack/eslint-patch": "^1.10.5",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.8.5",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@types/node": "^22.10.5",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"cypress": "^13.15.1",
"cypress": "^13.17.0",
"eslint": "^8.57.0",
"eslint-plugin-cypress": "^3.4.0",
"eslint-plugin-vue": "^9.30.0",
"jsdom": "^25.0.1",
"npm-run-all2": "^7.0.1",
"postcss": "^8.4.47",
"eslint-plugin-vue": "^9.32.0",
"jsdom": "^26.0.0",
"npm-run-all2": "^7.0.2",
"postcss": "^8.4.49",
"postcss-nesting": "^13.0.1",
"prettier": "^3.3.3",
"start-server-and-test": "^2.0.8",
"stylelint": "^16.9.0",
"prettier": "^3.4.2",
"start-server-and-test": "^2.0.9",
"stylelint": "^16.12.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2",
"tailwindcss": "^3.4.14",
"tailwindcss": "^3.4.17",
"typescript": "5.3.3",
"vite": "^5.4.8",
"vue-tsc": "^2.1.10"
"vue-tsc": "^2.2.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* ユーザーのロールを表す文字列列挙型です。
*/
export enum Roles {
ADMIN = 'ROLE_ADMIN',
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,16 @@ export const useAuthenticationStore = defineStore({
isAuthenticated(state) {
return state.authenticationState;
},
/**
* ユーザーが特定のロールに属するかどうかを判定する関数を取得します。
* ストアのゲッターには直接パラメーターを渡すことができないので、
* 関数を経由してパラメーターを受け取る必要があります。
* サンプルアプリでは、必ず Admin ロールを持つユーザーとしてログインするようになっています。
* @param state 状態。
* @returns ユーザーが特定のロールに属するかどうかを判定する関数。
*/
isInRole(state) {
return (role: string) => state.userRoles.includes(role);
},
},
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { flushPromises, mount, VueWrapper } from '@vue/test-utils';
import { router } from '@/router';
import { createPinia, setActivePinia } from 'pinia';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { createCustomErrorHandler } from '@/shared/error-handler/custom-error-handler';
import ItemsAddView from '@/views/catalog/ItemsAddView.vue';
import { Roles } from '@/shared/constants/roles';

async function getWrapper() {
const pinia = createPinia();
setActivePinia(pinia);
function CreateLoginState(userRoles: string[]) {
return createTestingPinia({
initialState: {
authentication: {
userRoles,
},
},
createSpy: vi.fn, // 明示的に設定する必要があります。
stubActions: false, // 結合テストなので、アクションはモック化しないように設定します。
});
}

async function getWrapper(pinia: TestingPinia) {
const customErrorHandler = createCustomErrorHandler();
return mount(ItemsAddView, {
global: { plugins: [pinia, router, customErrorHandler] },
});
}

describe('アイテムを追加できる', () => {
describe('管理者ロール_アイテムを追加できる', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
wrapper = await getWrapper();
loginState = CreateLoginState([Roles.ADMIN]);
wrapper = await getWrapper(loginState);
});

it('追加画面に遷移できる', async () => {
Expand All @@ -41,3 +54,29 @@ describe('アイテムを追加できる', () => {
expect(wrapper.html()).toContain('カタログアイテムを追加しました。');
});
});

describe('ゲストロール_アイテム追加ボタンが非活性', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
loginState = CreateLoginState(['ROLE_GUEST']);
wrapper = await getWrapper(loginState);
});

it('追加画面に遷移できる', async () => {
// Arrange
// Act
await flushPromises();
// Assert
expect(wrapper.html()).toContain('カタログアイテム追加');
});

it('追加ボタンが非活性', async () => {
// Arrange
// Act
const button = wrapper.find('button');
// Assert
expect(button.attributes('disabled')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { flushPromises, mount, VueWrapper } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { createCustomErrorHandler } from '@/shared/error-handler/custom-error-handler';
import ItemsEditView from '@/views/catalog/ItemsEditView.vue';
import { router } from '@/router';
import { Roles } from '@/shared/constants/roles';

async function getWrapper() {
const pinia = createPinia();
setActivePinia(pinia);
function CreateLoginState(userRoles: string[]) {
return createTestingPinia({
initialState: {
authentication: {
userRoles,
},
},
createSpy: vi.fn, // 明示的に設定する必要があります。
stubActions: false, // 結合テストなので、アクションはモック化しないように設定します。
});
}

async function getWrapper(pinia: TestingPinia) {
const customErrorHandler = createCustomErrorHandler();
router.push({ name: 'catalog/items/edit', params: { itemId: 1 } });
await router.isReady();
Expand All @@ -16,11 +27,13 @@ async function getWrapper() {
});
}

describe('アイテムが削除できる', () => {
describe('管理者ロール_アイテムが削除できる', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
wrapper = await getWrapper();
loginState = CreateLoginState([Roles.ADMIN]);
wrapper = await getWrapper(loginState);
});

it('編集画面に遷移できる', async () => {
Expand Down Expand Up @@ -76,11 +89,39 @@ describe('アイテムが削除できる', () => {
});
});

describe('アイテムが更新できる', () => {
describe('ゲストロール_アイテム削除ボタンが非活性', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
loginState = CreateLoginState(['ROLE_GUEST']);
wrapper = await getWrapper(loginState);
});

it('編集画面に遷移できる', async () => {
// Arrange
// Act
await flushPromises();
// Assert
expect(wrapper.html()).toContain('カタログアイテム編集');
});

it('削除ボタンが非活性', async () => {
// Arrange
// Act
const deleteButton = wrapper.findAll('button')[0];
// Assert
expect(deleteButton.attributes('disabled')).toBeDefined();
});
});

describe('管理者ロール_アイテムが更新できる', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
wrapper = await getWrapper();
loginState = CreateLoginState([Roles.ADMIN]);
wrapper = await getWrapper(loginState);
});

it('編集画面に遷移できる', async () => {
Expand Down Expand Up @@ -130,3 +171,29 @@ describe('アイテムが更新できる', () => {
).toBeFalsy();
});
});

describe('ゲストロール_アイテム更新ボタンが非活性', () => {
let loginState: TestingPinia;
let wrapper: VueWrapper;

beforeAll(async () => {
loginState = CreateLoginState(['ROLE_GUEST']);
wrapper = await getWrapper(loginState);
});

it('編集画面に遷移できる', async () => {
// Arrange
// Act
await flushPromises();
// Assert
expect(wrapper.html()).toContain('カタログアイテム編集');
});

it('更新ボタンが非活性', async () => {
// Arrange
// Act
const editButton = wrapper.findAll('button')[1];
// Assert
expect(editButton.attributes('disabled')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import {
fetchCategoriesAndBrands,
postCatalogItem,
Expand All @@ -14,9 +15,13 @@ import type {
GetCatalogBrandsResponse,
GetCatalogCategoriesResponse,
} from '@/generated/api-client';
import { useAuthenticationStore } from '@/stores/authentication/authentication';
import { Roles } from '@/shared/constants/roles';

const router = useRouter();
const customErrorHandler = useCustomErrorHandler();
const authenticationStore = useAuthenticationStore();
const { isInRole } = storeToRefs(authenticationStore);

const { errors, values, meta, defineField } = useForm({
validationSchema: catalogItemSchema,
Expand Down Expand Up @@ -211,7 +216,7 @@ onMounted(async () => {
<button
type="button"
class="rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-800 disabled:bg-blue-500 disabled:opacity-50"
:disabled="isInvalid()"
:disabled="isInvalid() || !isInRole(Roles.ADMIN)"
@click="AddItem()"
>
追加
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import {
fetchItem,
updateCatalogItem,
Expand All @@ -23,8 +24,12 @@ import type {
GetCatalogItemResponse,
} from '@/generated/api-client';
import { useCustomErrorHandler } from '@/shared/error-handler/use-custom-error-handler';
import { useAuthenticationStore } from '@/stores/authentication/authentication';
import { Roles } from '@/shared/constants/roles';

const customErrorHandler = useCustomErrorHandler();
const authenticationStore = useAuthenticationStore();
const { isInRole } = storeToRefs(authenticationStore);
const router = useRouter();
const route = useRoute();
const id = Number(route.params.itemId);
Expand Down Expand Up @@ -238,7 +243,7 @@ const deleteItemAsync = async () => {
} catch (error) {
if (error instanceof NotFoundError) {
customErrorHandler.handle(error, () => {
showToast('更新対象のカタログアイテムが見つかりませんでした。');
showToast('削除対象のカタログアイテムが見つかりませんでした。');
router.push({ name: '/catalog/items' });
});
} else if (error instanceof ConflictError) {
Expand Down Expand Up @@ -554,9 +559,11 @@ const updateItemAsync = async () => {
/>
</div>
<div class="flex justify-end">
<!-- サンプルアプリは必ず Admin ロールを持つユーザーとしてログインするようになっているので、削除ボタンが disable になることはありません。-->
<button
type="button"
class="rounded bg-red-800 px-4 py-2 font-bold text-white hover:bg-red-900"
class="rounded bg-red-800 px-4 py-2 font-bold text-white hover:bg-red-900 disabled:bg-red-500 disabled:opacity-50"
:disabled="!isInRole(Roles.ADMIN)"
@click="showDeleteConfirm = true"
>
削除
Expand All @@ -565,7 +572,7 @@ const updateItemAsync = async () => {
<button
type="button"
class="rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-800 disabled:bg-blue-500 disabled:opacity-50"
:disabled="isInvalid()"
:disabled="isInvalid() || !isInRole(Roles.ADMIN)"
@click="showUpdateConfirm = true"
>
更新
Expand Down
Loading
Loading