Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/web/app/components/graphql-queue/QueueContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ export const GraphQLQueueProvider = ({ parsedParams, boardDetails, children, bas

// Actions
addToQueue: (climb: Climb) => {
const newItem = createClimbQueueItem(climb, clientId, currentUserInfo);
const newItem = createClimbQueueItem({ ...climb, angle: parsedParams.angle }, clientId, currentUserInfo);

// Optimistic update
dispatch({ type: 'DELTA_ADD_QUEUE_ITEM', payload: { item: newItem } });
Expand All @@ -680,7 +680,7 @@ export const GraphQLQueueProvider = ({ parsedParams, boardDetails, children, bas
},

setCurrentClimb: async (climb: Climb) => {
const newItem = createClimbQueueItem(climb, clientId, currentUserInfo);
const newItem = createClimbQueueItem({ ...climb, angle: parsedParams.angle }, clientId, currentUserInfo);

// Optimistic update
dispatch({ type: 'SET_CURRENT_CLIMB', payload: newItem });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,72 @@ describe('queue-bridge-context', () => {
// New item becomes current
expect(newCurrent.climb.uuid).toBe('c2');
});

// ---------------------------------------------------------------
// Angle override behavior
// ---------------------------------------------------------------
describe('angle override', () => {
it('addToQueue overrides climb angle with current queue angle', () => {
// Current climb is at angle 40, new climb has angle 25
const currentClimb = createTestClimb({ uuid: 'c1', angle: 40 });
const item = createTestQueueItem(currentClimb, 'u1');
const climbWithDifferentAngle = createTestClimb({ uuid: 'c-new', angle: 25 });

const { result } = renderWithLocalQueue([item], item);
act(() => {
result.current!.addToQueue(climbWithDifferentAngle);
});

const [newQueue] = mockSetLocalQueueState.mock.calls[0];
// The added climb should have the queue's angle (40), not its own (25)
expect(newQueue[1].climb.angle).toBe(40);
expect(newQueue[1].climb.uuid).toBe('c-new');
});

it('setCurrentClimb overrides climb angle with current queue angle', () => {
// Current climb is at angle 40, proposal climb has angle 0
const currentClimb = createTestClimb({ uuid: 'c1', angle: 40 });
const item = createTestQueueItem(currentClimb, 'u1');
const proposalClimb = createTestClimb({ uuid: 'c-proposal', angle: 0 });

const { result } = renderWithLocalQueue([item], item);
act(() => {
result.current!.setCurrentClimb(proposalClimb);
});

const [, newCurrent] = mockSetLocalQueueState.mock.calls[0];
// The proposal climb should have the queue's angle (40), not 0
expect(newCurrent.climb.angle).toBe(40);
expect(newCurrent.climb.uuid).toBe('c-proposal');
});

it('preserves climb original angle when local queue is empty (no angle source)', () => {
// Empty queue — no current climb to derive angle from
const climbWithAngle = createTestClimb({ uuid: 'c-new', angle: 35 });

const { result } = renderWithLocalQueue([], null);
act(() => {
result.current!.addToQueue(climbWithAngle);
});

const [newQueue] = mockSetLocalQueueState.mock.calls[0];
// With no existing angle source, the climb keeps its original angle
expect(newQueue[0].climb.angle).toBe(35);
});

it('preserves climb original angle when setCurrentClimb on empty queue', () => {
const proposalClimb = createTestClimb({ uuid: 'c-proposal', angle: 25 });

const { result } = renderWithLocalQueue([], null);
act(() => {
result.current!.setCurrentClimb(proposalClimb);
});

const [, newCurrent] = mockSetLocalQueueState.mock.calls[0];
// With no existing angle source, the climb keeps its original angle
expect(newCurrent.climb.angle).toBe(25);
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ function usePersistentSessionQueueAdapter(): {
const angle: Angle = isParty
? ps.activeSession!.parsedParams.angle
: (ps.localCurrentClimbQueueItem?.climb?.angle ?? 0);
// Whether the angle comes from a real source (party session or existing queue item)
// vs the fallback 0 when the local queue is empty.
const hasAngleSource = isParty || !!ps.localCurrentClimbQueueItem;

const baseBoardPath = useMemo(() => {
if (isParty && ps.activeSession?.boardPath) {
Expand Down Expand Up @@ -114,7 +117,7 @@ function usePersistentSessionQueueAdapter(): {
(climb: Climb) => {
if (!boardDetails) return;
const newItem: ClimbQueueItem = {
climb,
climb: hasAngleSource ? { ...climb, angle } : climb,
addedBy: null,
uuid: uuidv4(),
suggested: false,
Expand All @@ -123,7 +126,7 @@ function usePersistentSessionQueueAdapter(): {
const current = currentClimbQueueItem ?? newItem;
ps.setLocalQueueState(newQueue, current, baseBoardPath, boardDetails);
},
[queue, currentClimbQueueItem, boardDetails, baseBoardPath, ps],
[queue, currentClimbQueueItem, boardDetails, baseBoardPath, ps, angle, hasAngleSource],
);

const removeFromQueue = useCallback(
Expand Down Expand Up @@ -166,7 +169,7 @@ function usePersistentSessionQueueAdapter(): {
(climb: Climb) => {
if (!boardDetails) return;
const newItem: ClimbQueueItem = {
climb,
climb: hasAngleSource ? { ...climb, angle } : climb,
addedBy: null,
uuid: uuidv4(),
suggested: false,
Expand All @@ -183,7 +186,7 @@ function usePersistentSessionQueueAdapter(): {
}
ps.setLocalQueueState(newQueue, newItem, baseBoardPath, boardDetails);
},
[queue, currentClimbQueueItem, boardDetails, baseBoardPath, ps],
[queue, currentClimbQueueItem, boardDetails, baseBoardPath, ps, angle, hasAngleSource],
);

// No-op functions for fields not used by the bottom bar — each matches its exact type signature
Expand Down
42 changes: 26 additions & 16 deletions packages/web/app/lib/__tests__/queue-storage-db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,35 @@ describe('getMostRecentQueue', () => {
it('returns the queue with the highest updatedAt', async () => {
const boardDetails = createTestBoardDetails();

await saveQueueState({
boardPath: '/kilter/1/10/1,2/40',
queue: [createTestClimbQueueItem('a')],
currentClimbQueueItem: null,
boardDetails,
updatedAt: 1000,
});
await saveQueueState({
boardPath: '/tension/1/10/1,2/45',
queue: [createTestClimbQueueItem('b')],
currentClimbQueueItem: null,
boardDetails: createTestBoardDetails({ board_name: 'tension' }),
updatedAt: 2000,
});
// Write directly to IndexedDB with controlled timestamps to avoid
// Date.now() non-determinism in saveQueueState (which overwrites updatedAt).
const db = await openDB(DB_NAME, 1);
await db.put(
STORE_NAME,
{
boardPath: '/kilter/1/10/1,2/40',
queue: [createTestClimbQueueItem('a')],
currentClimbQueueItem: null,
boardDetails,
updatedAt: 1000,
},
'queue:/kilter/1/10/1,2/40',
);
await db.put(
STORE_NAME,
{
boardPath: '/tension/1/10/1,2/45',
queue: [createTestClimbQueueItem('b')],
currentClimbQueueItem: null,
boardDetails: createTestBoardDetails({ board_name: 'tension' }),
updatedAt: 2000,
},
'queue:/tension/1/10/1,2/45',
);
db.close();

const result = await getMostRecentQueue();
expect(result).not.toBeNull();
// saveQueueState overwrites updatedAt with Date.now(), but the second call
// has a later Date.now() so it will have the higher timestamp
expect(result!.boardPath).toBe('/tension/1/10/1,2/45');
expect(result!.queue).toHaveLength(1);
expect(result!.queue[0].uuid).toBe('b');
Expand Down
Loading