-
Notifications
You must be signed in to change notification settings - Fork 3
Fix featured list channel sorting #2559
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { faker } from "@faker-js/faker/locale/en" | ||
import { randomizeGroups, hasPosition } from "./util" | ||
|
||
faker.seed(12345) // Seed faker for consistent test results | ||
jest | ||
.spyOn(Math, "random") | ||
.mockImplementation(() => faker.number.float({ min: 0, max: 1 })) | ||
|
||
describe("randomizeGroups", () => { | ||
it("should group by position and randomize within groups with duplicates", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically does not test that items are randomized within their groups (though not very testable!) |
||
const items = [ | ||
{ id: "a1", position: 1 }, | ||
{ id: "a2", position: 1 }, | ||
{ id: "b1", position: 12 }, | ||
{ id: "b2", position: 12 }, | ||
{ id: "c1", position: 2 }, | ||
{ id: "c2", position: 2 }, | ||
{ id: "d1", position: 3 }, | ||
] | ||
|
||
const result = randomizeGroups(items) | ||
|
||
// Should be grouped by position in numerical order | ||
expect(result[0].position).toBe(1) | ||
expect(result[1].position).toBe(1) | ||
expect(result[2].position).toBe(2) | ||
expect(result[3].position).toBe(2) | ||
expect(result[4].position).toBe(3) | ||
expect(result[5].position).toBe(12) | ||
expect(result[6].position).toBe(12) | ||
|
||
// Should contain all items | ||
expect(result).toHaveLength(7) | ||
expect(result.map((item) => item.id).sort()).toEqual([ | ||
"a1", | ||
"a2", | ||
"b1", | ||
"b2", | ||
"c1", | ||
"c2", | ||
"d1", | ||
]) | ||
}) | ||
|
||
it("should handle positions greater than 10 correctly (avoid lexicographical sorting)", () => { | ||
const items = [ | ||
{ id: "item15", position: 15 }, | ||
{ id: "item2", position: 2 }, | ||
{ id: "item11", position: 11 }, | ||
{ id: "item1", position: 1 }, | ||
{ id: "item20", position: 20 }, | ||
] | ||
|
||
const result = randomizeGroups(items) | ||
|
||
// Should be numerically sorted: 1, 2, 11, 15, 20 | ||
// NOT lexicographically sorted: 1, 11, 15, 2, 20 | ||
const positions = result.map((item) => item.position) | ||
expect(positions).toEqual([1, 2, 11, 15, 20]) | ||
}) | ||
|
||
it("should handle empty array", () => { | ||
const items: Array<{ id: string; position: number }> = [] | ||
const result = randomizeGroups(items) | ||
expect(result).toEqual([]) | ||
}) | ||
}) | ||
|
||
describe("hasPosition", () => { | ||
it("should return true for objects with non-null position", () => { | ||
const obj = { id: "test", position: 5 } | ||
expect(hasPosition(obj)).toBe(true) | ||
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. copilot really wanted to add this test...... |
||
}) | ||
|
||
it("should return false for objects with null position", () => { | ||
const obj = { id: "test", position: null } | ||
expect(hasPosition(obj)).toBe(false) | ||
}) | ||
|
||
it("should return true for objects with position 0", () => { | ||
const obj = { id: "test", position: 0 } | ||
expect(hasPosition(obj)).toBe(true) | ||
}) | ||
|
||
it("should act as a type guard", () => { | ||
const obj: { id: string; position: number | null } = { | ||
id: "test", | ||
position: 5, | ||
} | ||
|
||
if (hasPosition(obj)) { | ||
// TypeScript should now know that obj.position is number, not number | null | ||
const position: number = obj.position | ||
expect(position).toBe(5) | ||
} | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
const hasPosition = <T extends { position: number | null }>( | ||
r: T, | ||
): r is T & { position: number } => r.position !== null | ||
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type guard+narrowing... nice! |
||
|
||
const shuffle = ([...arr]) => { | ||
let m = arr.length | ||
while (m) { | ||
const i = Math.floor(Math.random() * m--) | ||
;[arr[m], arr[i]] = [arr[i], arr[m]] | ||
} | ||
return arr | ||
} | ||
|
||
/** | ||
* Randomize a group of ordered items, where item positions might be duplicated. | ||
* Ordering is preserved between groups, but randomized within groups. | ||
* | ||
* E.g., given the items | ||
* [ | ||
* { id: 1, position: 1 }, | ||
* { id: 2, position: 1 }, | ||
* { id: 3, position: 2 }, | ||
* { id: 4, position: 3 }, | ||
* { id: 5, position: 3 }, | ||
* { id: 6, position: 3 }, | ||
* ] | ||
* | ||
* The results would be: | ||
* [ | ||
* ...items with position 1 in random order..., | ||
* ...items with position 2 in random order..., | ||
* ...items with position 3 in random order... | ||
* ] | ||
*/ | ||
const randomizeGroups = <T extends { position: number }>(results: T[]): T[] => { | ||
const resultsByPosition: { | ||
[position: string]: T[] | undefined | ||
} = {} | ||
const randomizedResults: T[] = [] | ||
results.forEach((result) => { | ||
const pos = result?.position | ||
if (!resultsByPosition[pos]) { | ||
resultsByPosition[pos] = [] | ||
} | ||
resultsByPosition[pos]?.push(result) | ||
}) | ||
Object.keys(resultsByPosition) | ||
.sort( | ||
(a, b) => Number(a) - Number(b), // Sort positions numerically | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only functional change vs the old implementation. But I decided to move it to a separate file and have copilot add some tests. |
||
) | ||
.forEach((position) => { | ||
const shuffled = shuffle(resultsByPosition[position] ?? []) | ||
randomizedResults.push(...shuffled) | ||
}) | ||
return randomizedResults | ||
} | ||
|
||
export { randomizeGroups, hasPosition } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explanation: Object keys are strings, and these were stringified numbers, so they were being sorted like
0, 1, 10, 11, 2, 3, 4, ...
.This wasn't a visible problem on the homepage because that carusel uses items from every offeror... to get up to 12 items, that carousel only needs positions 0, 1, 2 from all five offerors.
The featured list query is also used on channel pages (We don't query the learning path directly because, at least right now, those are private; only visible to non-staff via
/featured
endpoint). And on the channel pages, 12 items are shown, so the sorting was getting messed up.