From f8bf5deb1bb6220f7ae7db8d93fafabd21472ba6 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 11 Jun 2026 16:15:34 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E3=83=9E=E3=82=A4=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E6=B3=A8=E6=96=87=E5=B1=A5=E6=AD=B4=E8=A9=B3?= =?UTF-8?q?=E7=B4=B0=E3=81=AB=E3=81=8A=E5=95=8F=E3=81=84=E5=90=88=E3=82=8F?= =?UTF-8?q?=E3=81=9B=E7=95=AA=E5=8F=B7=E3=82=92=E8=A1=A8=E7=A4=BA=20(#6818?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit フロント会員がマイページの注文履歴詳細画面で配送のお問い合わせ番号 (追跡番号) を確認できるようにする。 - history.twig: 配送情報ループ内、お届け時間の直後に表示ブロックを追加。 値が未入力 (null/空文字列) の場合は項目自体を非表示。複数配送先は 配送先ごとに評価。出力は自動エスケープ (XSS 対策)。 - messages.ja/en.yaml: front.mypage.tracking_number を追加 (お問い合わせ番号 / Tracking No.、admin.order.tracking_number と統一)。 - MypageControllerTest: 表示/null非表示/空文字非表示/複数配送/一部のみ入力/ XSSエスケープ/翻訳定義 の主要ケースを追加。非表示判定はセレクタ件数で検証し、 受注生成・配送追加・履歴取得をヘルパーへ集約。 - front-mypage.spec.ts: 管理画面でお問い合わせ番号を設定しマイページで 表示を確認する E2E を追加。受注番号をキーに検索・直接遷移することで 一覧の並び順に依存しない (フレーキー回避)。 データソースは既存の dtb_shipping.tracking_number。DB・エンティティ・ コントローラ・ルートの変更は不要 (表示のみ)。 Co-Authored-By: Claude Opus 4.8 --- e2e/tests/front-mypage.spec.ts | 104 +++++++++++ src/Eccube/Resource/locale/messages.en.yaml | 1 + src/Eccube/Resource/locale/messages.ja.yaml | 1 + .../template/default/Mypage/history.twig | 6 + .../Tests/Web/Mypage/MypageControllerTest.php | 167 ++++++++++++++++++ 5 files changed, 279 insertions(+) diff --git a/e2e/tests/front-mypage.spec.ts b/e2e/tests/front-mypage.spec.ts index 3ff06285b42..003a10146ac 100644 --- a/e2e/tests/front-mypage.spec.ts +++ b/e2e/tests/front-mypage.spec.ts @@ -144,6 +144,68 @@ async function addDeliveryAddress(page: Page, addr01: string) { await expect(page.locator('div.ec-pageHeader h1')).toContainText('お届け先一覧'); } +/** + * Helper: Return the order number of the most recently created order for the + * logged-in customer by reading the first "詳細を見る" link on the order history. + * The order detail link is /mypage/history/{order_no}. + */ +async function getLatestOrderNo(page: Page): Promise { + await page.goto('/mypage/'); + await page.waitForLoadState('load'); + const href = await page.locator('p.ec-historyListHeader__action a').first().getAttribute('href'); + const matched = href?.match(/\/mypage\/history\/(.+)$/); + expect(matched, `order detail link should contain an order_no: ${href}`).not.toBeNull(); + return matched![1]; +} + +/** + * Helper: Set the tracking number (お問い合わせ番号) of the given order's first + * shipping via the admin order/shipping edit screen. Opens a separate admin page, + * logs in, searches for the order by its order number (so the test never depends + * on global ordering), and saves the tracking number. + */ +async function setTrackingNumberViaAdmin(page: Page, orderNo: string, trackingNumber: string) { + const adminRoute = process.env.ECCUBE_ADMIN_ROUTE || 'admin'; + + const adminPage = await page.context().newPage(); + await adminPage.goto(`/${adminRoute}/`); + await adminPage.waitForLoadState('load'); + await adminPage.locator('#login_id').fill(process.env.ADMIN_USER || 'admin'); + await adminPage.locator('#password').fill(process.env.ADMIN_PASSWORD || 'password'); + await adminPage.getByRole('button', { name: 'ログイン' }).click(); + await adminPage.waitForLoadState('load'); + + // Search the order by its order number and open that specific order + await adminPage.goto(`/${adminRoute}/order`); + await adminPage.waitForLoadState('load'); + await adminPage.locator('#admin_search_order_multi').fill(orderNo); + await adminPage.locator('#search_form #search_submit').click(); + await adminPage.waitForLoadState('load'); + + const orderLink = adminPage.locator('table tbody td a[href*="/order/"]').first(); + await expect(orderLink).toBeVisible(); + const orderEditHref = await orderLink.getAttribute('href'); + await adminPage.goto(orderEditHref!); + await adminPage.waitForLoadState('load'); + + // Navigate to the shipping edit page + const shippingEditLink = adminPage.locator('a[href*="/shipping/"][href*="/edit"]'); + await expect(shippingEditLink).toBeVisible(); + const shippingEditHref = await shippingEditLink.getAttribute('href'); + await adminPage.goto(shippingEditHref!); + await adminPage.waitForLoadState('load'); + await expect(adminPage.locator('.c-pageTitle')).toContainText('出荷登録'); + + // Fill the tracking number and save + await adminPage.locator('#form_shippings_0_tracking_number').fill(trackingNumber); + await adminPage.locator('button.ladda-button[type="submit"]').click(); + await adminPage.waitForLoadState('load'); + await expect(adminPage.locator('.alert-success')).toContainText('保存しました', { timeout: 30_000 }); + await expect(adminPage.locator('#form_shippings_0_tracking_number')).toHaveValue(trackingNumber); + + await adminPage.close(); +} + test.describe('Front Mypage (EF05)', () => { test('EF0501-UC01-T01 Mypage 初期表示', async ({ page }) => { @@ -213,6 +275,48 @@ test.describe('Front Mypage (EF05)', () => { await expect(totalBox).toContainText('合計'); }); + test('EF0503-UC01-T03 Mypage ご注文履歴詳細 お問い合わせ番号表示', async ({ page }) => { + const trackingNumber = '1234567890123'; + + await loginAsTestCustomer(page); + + // Create an order, capture its order number, then set its tracking number via admin + await createOrder(page); + const orderNo = await getLatestOrderNo(page); + await setTrackingNumberViaAdmin(page, orderNo, trackingNumber); + + // Open that specific order's detail page directly (no dependency on list ordering) + await page.goto(`/mypage/history/${orderNo}`); + await page.waitForLoadState('load'); + await expect(page.locator('div.ec-pageHeader h1')).toContainText('ご注文履歴詳細'); + + // The tracking number label and value are shown in the delivery section + const delivery = page.locator('div.ec-orderDelivery'); + await expect(delivery).toContainText('お問い合わせ番号'); + await expect(delivery).toContainText(trackingNumber); + + // The tracking number is displayed right after the delivery time item + const labels = delivery.locator('div.ec-definitions--soft dt'); + const deliveryTimeIndex = (await labels.allTextContents()).findIndex(t => t.includes('お届け時間')); + expect(deliveryTimeIndex).toBeGreaterThanOrEqual(0); + await expect(labels.nth(deliveryTimeIndex + 1)).toContainText('お問い合わせ番号'); + }); + + test('EF0503-UC01-T04 Mypage ご注文履歴詳細 お問い合わせ番号未設定は非表示', async ({ page }) => { + await loginAsTestCustomer(page); + + // Create an order without a tracking number, then open that specific order + await createOrder(page); + const orderNo = await getLatestOrderNo(page); + await page.goto(`/mypage/history/${orderNo}`); + await page.waitForLoadState('load'); + await expect(page.locator('div.ec-pageHeader h1')).toContainText('ご注文履歴詳細'); + + // The tracking number item is not rendered + const delivery = page.locator('div.ec-orderDelivery'); + await expect(delivery).not.toContainText('お問い合わせ番号'); + }); + test('EF0503-UC01-T02 Mypage お気に入り一覧', async ({ page }) => { await loginAsTestCustomer(page); diff --git a/src/Eccube/Resource/locale/messages.en.yaml b/src/Eccube/Resource/locale/messages.en.yaml index 3807b8b6380..7ffaac7ed5e 100644 --- a/src/Eccube/Resource/locale/messages.en.yaml +++ b/src/Eccube/Resource/locale/messages.en.yaml @@ -246,6 +246,7 @@ front.mypage.delivery: Delivery to front.mypage.delivery_provider: Delivery Method front.mypage.delivery_date: Delivery Date front.mypage.delivery_time: Delivery Time +front.mypage.tracking_number: Tracking No. front.mypage.current_price: [Current Price] front.mypage.reorder: Reorder front.mypage.payment_info: Payment Info diff --git a/src/Eccube/Resource/locale/messages.ja.yaml b/src/Eccube/Resource/locale/messages.ja.yaml index 96445e787c0..fb19328503a 100644 --- a/src/Eccube/Resource/locale/messages.ja.yaml +++ b/src/Eccube/Resource/locale/messages.ja.yaml @@ -246,6 +246,7 @@ front.mypage.delivery: お届け先 front.mypage.delivery_provider: 配送方法 front.mypage.delivery_date: お届け日 front.mypage.delivery_time: お届け時間 +front.mypage.tracking_number: お問い合わせ番号 front.mypage.current_price: 【現在価格】 front.mypage.reorder: 再注文する front.mypage.payment_info: お支払い情報 diff --git a/src/Eccube/Resource/template/default/Mypage/history.twig b/src/Eccube/Resource/template/default/Mypage/history.twig index 7ad1b0fc34d..c08d98af35e 100644 --- a/src/Eccube/Resource/template/default/Mypage/history.twig +++ b/src/Eccube/Resource/template/default/Mypage/history.twig @@ -119,6 +119,12 @@ file that was distributed with this source code.
{{ 'front.mypage.delivery_time'|trans }} :
{{ Shipping.shipping_delivery_time|default('common.select__unspecified'|trans) }}
+ {% if Shipping.tracking_number %} +
+
{{ 'front.mypage.tracking_number'|trans }} :
+
{{ Shipping.tracking_number }}
+
+ {% endif %} {% endfor %}
diff --git a/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php b/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php index 69c4dccca46..15b3f05c36c 100644 --- a/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php +++ b/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php @@ -18,10 +18,13 @@ use Eccube\Entity\Customer; use Eccube\Entity\CustomerFavoriteProduct; use Eccube\Entity\Master\OrderStatus; +use Eccube\Entity\Order; use Eccube\Entity\Product; +use Eccube\Entity\Shipping; use Eccube\Tests\Fixture\Generator; use Eccube\Tests\Web\AbstractWebTestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\Translation\TranslatorInterface; final class MypageControllerTest extends AbstractWebTestCase { @@ -158,6 +161,170 @@ public function testHistoryWithNotFound() $this->verify(); } + /** + * 注文履歴詳細を閲覧可能なステータス (NEW) の受注を生成する. + * + * createOrder() の既定ステータスは PROCESSING (仮受注) で mypage_history が 404 になるため, + * NEW へ遷移させてから返す. + */ + private function createOrderForHistory(): Order + { + $Order = $this->createOrder($this->Customer); + $Order->setOrderStatus($this->entityManager->find(OrderStatus::class, OrderStatus::NEW)); + + return $Order; + } + + /** + * 受注に 2 つ目の配送先を追加し, お問い合わせ番号を設定する. + */ + private function addShipping(Order $Order, ?string $trackingNumber): Shipping + { + /** @var Shipping $Base */ + $Base = $Order->getShippings()->first(); + + $Shipping = new Shipping(); + $Shipping->copyProperties($this->Customer); + $Shipping + ->setOrder($Order) + ->setPref($Base->getPref()) + ->setDelivery($Base->getDelivery()) + ->setShippingDeliveryName($Base->getShippingDeliveryName()) + ->setTrackingNumber($trackingNumber) + ->setCreateDate(new \DateTime()) + ->setUpdateDate(new \DateTime()); + $Order->addShipping($Shipping); + $this->entityManager->persist($Shipping); + + return $Shipping; + } + + /** + * 注文履歴詳細をログイン会員として取得する. + */ + private function requestHistory(Order $Order): \Symfony\Component\DomCrawler\Crawler + { + $this->loginTo($this->Customer); + + return $this->client->request( + Request::METHOD_GET, + $this->generateUrl('mypage_history', ['order_no' => $Order->getOrderNo()]) + ); + } + + /** + * お問い合わせ番号が入力されている場合, 注文履歴詳細に表示される. + */ + public function testHistoryWithTrackingNumber() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber('1234567890123'); + $this->entityManager->flush(); + + $crawler = $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertCount(1, $crawler->filter('dt:contains("お問い合わせ番号")')); + $this->assertStringContainsString('1234567890123', $crawler->text()); + } + + /** + * お問い合わせ番号が null の場合, 項目自体が表示されない. + */ + public function testHistoryWithoutTrackingNumber() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber(null); + $this->entityManager->flush(); + + $crawler = $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertCount(0, $crawler->filter('dt:contains("お問い合わせ番号")')); + } + + /** + * お問い合わせ番号が空文字列の場合, 項目自体が表示されない. + */ + public function testHistoryWithEmptyTrackingNumber() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber(''); + $this->entityManager->flush(); + + $crawler = $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertCount(0, $crawler->filter('dt:contains("お問い合わせ番号")')); + } + + /** + * 複数配送先がある場合, 入力済みの配送先ごとにお問い合わせ番号が表示される. + */ + public function testHistoryWithMultipleShippings() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber('1111111111111'); + $this->addShipping($Order, '2222222222222'); + $this->entityManager->flush(); + + $crawler = $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertStringContainsString('1111111111111', $crawler->text()); + $this->assertStringContainsString('2222222222222', $crawler->text()); + // 配送先ごとに「お問い合わせ番号」ラベルが出力される (2 件). + $this->assertCount(2, $crawler->filter('dt:contains("お問い合わせ番号")')); + } + + /** + * 複数配送先のうち一部のみ入力されている場合, 入力済みの配送先のみ表示される. + */ + public function testHistoryWithMultipleShippingsPartiallyFilled() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber('1111111111111'); + // 2 つ目の配送先はお問い合わせ番号未入力. + $this->addShipping($Order, null); + $this->entityManager->flush(); + + $crawler = $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertStringContainsString('1111111111111', $crawler->text()); + // 入力済みは 1 件のみ. + $this->assertCount(1, $crawler->filter('dt:contains("お問い合わせ番号")')); + } + + /** + * お問い合わせ番号に HTML 特殊文字が含まれていても自動エスケープされ, スクリプトが実行されない. + */ + public function testHistoryTrackingNumberXss() + { + $Order = $this->createOrderForHistory(); + $Order->getShippings()->first()->setTrackingNumber(''); + $this->entityManager->flush(); + + $this->requestHistory($Order); + + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $html = $this->client->getResponse()->getContent(); + $this->assertStringContainsString('<script>', $html); + $this->assertStringNotContainsString(''); @@ -309,14 +310,14 @@ public function testHistoryTrackingNumberXss() $this->assertTrue($this->client->getResponse()->isSuccessful()); $html = $this->client->getResponse()->getContent(); - $this->assertStringContainsString('<script>', $html); - $this->assertStringNotContainsString('