diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 0c8cd8bfb5b..80443a8a8bb 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -30,6 +30,7 @@ jobs: - front-product - front-customer - front-other + - front-mypage include: - db: pgsql database_url: postgres://postgres:password@127.0.0.1:5432/eccube_db diff --git a/.gitignore b/.gitignore index 52dcccb2915..951a82a3a17 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ node_modules .phpunit /phpunit.xml ###< symfony/phpunit-bridge ### +/.phpunit.result.cache ###> friendsofphp/php-cs-fixer ### /.php-cs-fixer.cache diff --git a/e2e/tests/front-mypage.spec.ts b/e2e/tests/front-mypage.spec.ts index 3ff06285b42..e0f40e9bbaf 100644 --- a/e2e/tests/front-mypage.spec.ts +++ b/e2e/tests/front-mypage.spec.ts @@ -144,6 +144,71 @@ 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'); + + // Target the row whose order number cell matches orderNo, then its order link + // (so we never follow the wrong row even if the search returns multiple). + const orderRow = adminPage.locator('table tbody tr', { hasText: orderNo }); + const orderLink = orderRow.locator('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 +278,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 (structural check on the label dt) + const delivery = page.locator('div.ec-orderDelivery'); + await expect(delivery.locator('dt', { hasText: 'お問い合わせ番号' })).toHaveCount(0); + }); + 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..47905805247 100644 --- a/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php +++ b/tests/Eccube/Tests/Web/Mypage/MypageControllerTest.php @@ -18,10 +18,14 @@ 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\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\Translation\TranslatorInterface; final class MypageControllerTest extends AbstractWebTestCase { @@ -158,6 +162,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): Crawler + { + $this->loginTo($this->Customer); + + return $this->client->request( + Request::METHOD_GET, + $this->generateUrl('mypage_history', ['order_no' => $Order->getOrderNo()]) + ); + } + + /** + * お問い合わせ番号が入力されている場合, 注文履歴詳細に表示される. + */ + public function testHistoryWithTrackingNumber(): void + { + $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(): void + { + $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(): void + { + $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(): void + { + $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(): void + { + $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(): void + { + $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>', (string) $html); + $this->assertStringNotContainsString('