diff --git a/samples/Dressca/dressca-frontend/admin/mock/handlers/users.ts b/samples/Dressca/dressca-frontend/admin/mock/handlers/users.ts index 7dd5668df..b02645d86 100644 --- a/samples/Dressca/dressca-frontend/admin/mock/handlers/users.ts +++ b/samples/Dressca/dressca-frontend/admin/mock/handlers/users.ts @@ -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: 'admin@example.com', - roles: ['Admin'], + roles: [Roles.ADMIN], }; export const usersHandlers = [ diff --git a/samples/Dressca/dressca-frontend/admin/package.json b/samples/Dressca/dressca-frontend/admin/package.json index 96deeb5f4..37f4b9e6c 100644 --- a/samples/Dressca/dressca-frontend/admin/package.json +++ b/samples/Dressca/dressca-frontend/admin/package.json @@ -22,16 +22,16 @@ "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": [ @@ -39,36 +39,37 @@ ] }, "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" } } diff --git a/samples/Dressca/dressca-frontend/admin/src/shared/constants/roles.ts b/samples/Dressca/dressca-frontend/admin/src/shared/constants/roles.ts new file mode 100644 index 000000000..91714ab9e --- /dev/null +++ b/samples/Dressca/dressca-frontend/admin/src/shared/constants/roles.ts @@ -0,0 +1,6 @@ +/** + * ユーザーのロールを表す文字列列挙型です。 + */ +export enum Roles { + ADMIN = 'ROLE_ADMIN', +} diff --git a/samples/Dressca/dressca-frontend/admin/src/stores/authentication/authentication.ts b/samples/Dressca/dressca-frontend/admin/src/stores/authentication/authentication.ts index 2d504d4e2..9afa5a607 100644 --- a/samples/Dressca/dressca-frontend/admin/src/stores/authentication/authentication.ts +++ b/samples/Dressca/dressca-frontend/admin/src/stores/authentication/authentication.ts @@ -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); + }, }, }); diff --git a/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsAddView.spec.ts b/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsAddView.spec.ts index 826bf6e50..cf0d7c58e 100644 --- a/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsAddView.spec.ts +++ b/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsAddView.spec.ts @@ -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 () => { @@ -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(); + }); +}); diff --git a/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsEditView.spec.ts b/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsEditView.spec.ts index 62d8ff1f5..e37ccf4b0 100644 --- a/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsEditView.spec.ts +++ b/samples/Dressca/dressca-frontend/admin/src/views/__tests__/ItemsEditView.spec.ts @@ -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(); @@ -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 () => { @@ -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 () => { @@ -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(); + }); +}); diff --git a/samples/Dressca/dressca-frontend/admin/src/views/catalog/ItemsAddView.vue b/samples/Dressca/dressca-frontend/admin/src/views/catalog/ItemsAddView.vue index 617d58444..8fa7e1ea6 100644 --- a/samples/Dressca/dressca-frontend/admin/src/views/catalog/ItemsAddView.vue +++ b/samples/Dressca/dressca-frontend/admin/src/views/catalog/ItemsAddView.vue @@ -1,5 +1,6 @@ + diff --git a/samples/Dressca/dressca-frontend/consumer/src/views/ordering/CheckoutView.vue b/samples/Dressca/dressca-frontend/consumer/src/views/ordering/CheckoutView.vue index 48575743a..76c0cf7b7 100644 --- a/samples/Dressca/dressca-frontend/consumer/src/views/ordering/CheckoutView.vue +++ b/samples/Dressca/dressca-frontend/consumer/src/views/ordering/CheckoutView.vue @@ -5,12 +5,14 @@ import { useUserStore } from '@/stores/user/user'; import { postOrder } from '@/services/ordering/ordering-service'; import { fetchBasket } from '@/services/basket/basket-service'; import { showToast } from '@/services/notification/notificationService'; - import { useRouter } from 'vue-router'; import { currencyHelper } from '@/shared/helpers/currencyHelper'; import { assetHelper } from '@/shared/helpers/assetHelper'; import { storeToRefs } from 'pinia'; import { useCustomErrorHandler } from '@/shared/error-handler/use-custom-error-handler'; +import { i18n } from '@/locales/i18n'; +import { errorMessageFormat } from '@/shared/error-handler/error-message-format'; +import { HttpError } from '@/shared/error-handler/custom-error'; const userStore = useUserStore(); const basketStore = useBasketStore(); @@ -21,6 +23,7 @@ const router = useRouter(); const customErrorHandler = useCustomErrorHandler(); const { toCurrencyJPY } = currencyHelper(); const { getFirstAssetUrl } = assetHelper(); +const { t } = i18n.global; const checkout = async () => { try { @@ -33,13 +36,32 @@ const checkout = async () => { ); router.push({ name: 'ordering/done', params: { orderId } }); } catch (error) { - customErrorHandler.handle(error, () => { - showToast('注文に失敗しました。'); - router.push({ name: 'error' }); - }); + customErrorHandler.handle( + error, + () => { + router.push({ name: 'error' }); + }, + (httpError: HttpError) => { + if (!httpError.response?.exceptionId) { + showToast(t('failedToOrderItems')); + } else { + const message = errorMessageFormat( + httpError.response.exceptionId, + httpError.response.exceptionValues, + ); + showToast( + message, + httpError.response.exceptionId, + httpError.response.title, + httpError.response.detail, + httpError.response.status, + 100000, + ); + } + }, + ); } }; - onMounted(async () => { await fetchBasket(); if (getBasket.value.basketItems?.length === 0) { @@ -51,7 +73,7 @@ onMounted(async () => {