From e68f24a717400a6e0d08401f1bf017a1aff24dda Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Fri, 28 Feb 2025 18:21:53 +0800 Subject: [PATCH] serializers/crate: Prevent `categories` and `keywords` from being cleared Previously, if `categories` and `keywords` of `crate` had existing values, they would be cleared from query results if those value returned null. This caused issues when `crate` was peeked from the store. This commit detects null values and ignores the fields instead of updating them. --- app/serializers/crate.js | 18 ++++++++++++++++++ e2e/acceptance/crate.spec.ts | 29 +++++++++++++++++++++++++++++ tests/acceptance/crate-test.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/app/serializers/crate.js b/app/serializers/crate.js index ab6d84602a3..b0bca2c24e6 100644 --- a/app/serializers/crate.js +++ b/app/serializers/crate.js @@ -1,5 +1,7 @@ import ApplicationSerializer from './application'; +const SKIP_NULL_FIELDS = new Set(['categories', 'keywords']); + export default class CrateSerializer extends ApplicationSerializer { isNewSerializerAPI = true; @@ -10,4 +12,20 @@ export default class CrateSerializer extends ApplicationSerializer { return super.extractRelationships(...arguments); } + + normalizeQueryResponse(_store, _modelClass, payload) { + // We don't want existing relationships overwritten by results with null values. + // See: https://github.com/rust-lang/crates.io/issues/10711 + if (payload.crates) { + payload.crates = payload.crates.map(crate => { + for (const rel of SKIP_NULL_FIELDS) { + if (crate[rel] === null) { + delete crate[rel]; + } + } + return crate; + }); + } + return super.normalizeQueryResponse(...arguments); + } } diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts index c943cbbb39f..bf09fe3628b 100644 --- a/e2e/acceptance/crate.spec.ts +++ b/e2e/acceptance/crate.spec.ts @@ -270,4 +270,33 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page).toHaveURL('/crates/nanomsg'); await expect(page.locator('[data-test-keyword]')).toBeVisible(); }); + + test('keywords are shown when navigating from crate to keywords, and then back to crate', async ({ page, msw }) => { + loadFixtures(msw.db); + + await page.goto('/crates/nanomsg'); + await expect(page.locator('[data-test-keyword]')).toBeVisible(); + + await page.getByRole('link', { name: '#network', exact: true }).click(); + await expect(page).toHaveURL('/keywords/network'); + await page.getByRole('link', { name: 'nanomsg', exact: true }).click(); + + await expect(page).toHaveURL('/crates/nanomsg'); + await expect(page.locator('[data-test-keyword]')).toBeVisible(); + }); + + test('keywords are shown when navigating from crate to searchs, and then back to crate', async ({ page, msw }) => { + loadFixtures(msw.db); + + await page.goto('/crates/nanomsg'); + await expect(page.locator('[data-test-keyword]')).toBeVisible(); + + await page.fill('[data-test-search-input]', 'nanomsg'); + await page.locator('[data-test-search-form]').getByRole('button', { name: 'Submit' }).click(); + await expect(page).toHaveURL('/search?q=nanomsg'); + await page.getByRole('link', { name: 'nanomsg', exact: true }).click(); + + await expect(page).toHaveURL('/crates/nanomsg'); + await expect(page.locator('[data-test-keyword]')).toBeVisible(); + }); }); diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index 30ca2756f83..d3e03704ba9 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -1,4 +1,4 @@ -import { click, currentRouteName, currentURL, waitFor } from '@ember/test-helpers'; +import { click, currentRouteName, currentURL, fillIn, triggerEvent, waitFor } from '@ember/test-helpers'; import { module, skip, test } from 'qunit'; import { loadFixtures } from '@crates-io/msw/fixtures.js'; @@ -271,4 +271,33 @@ module('Acceptance | crate page', function (hooks) { assert.strictEqual(currentURL(), '/crates/nanomsg'); assert.dom('[data-test-keyword]').exists(); }); + + test('keywords are shown when navigating from crate to keywords, and then back to crate', async function (assert) { + loadFixtures(this.db); + + await visit('/crates/nanomsg'); + assert.dom('[data-test-keyword]').exists(); + + await click('[data-test-keyword="network"]'); + assert.strictEqual(currentURL(), '/keywords/network'); + await click('[href="/crates/nanomsg"]'); + + assert.strictEqual(currentURL(), '/crates/nanomsg'); + assert.dom('[data-test-keyword]').exists(); + }); + + test('keywords are shown when navigating from crate to searchs, and then back to crate', async function (assert) { + loadFixtures(this.db); + + await visit('/crates/nanomsg'); + assert.dom('[data-test-keyword]').exists(); + + await fillIn('[data-test-search-input]', 'nanomsg'); + await triggerEvent('[data-test-search-form]', 'submit'); + assert.strictEqual(currentURL(), '/search?q=nanomsg'); + await click('[href="/crates/nanomsg"]'); + + assert.strictEqual(currentURL(), '/crates/nanomsg'); + assert.dom('[data-test-keyword]').exists(); + }); });