Merge branch 'main' into livekit-experiment
This commit is contained in:
272
test/otel/ObjectFlattener-test.ts
Normal file
272
test/otel/ObjectFlattener-test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import {
|
||||
AudioConcealment,
|
||||
ByteSentStatsReport,
|
||||
ConnectionStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||
import { ObjectFlattener } from "../../src/otel/ObjectFlattener";
|
||||
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
describe("ObjectFlattener", () => {
|
||||
const noConcealment: AudioConcealment = {
|
||||
concealedAudio: 0,
|
||||
totalAudioDuration: 0,
|
||||
};
|
||||
|
||||
const statsReport: GroupCallStatsReport<ConnectionStatsReport> = {
|
||||
report: {
|
||||
callId: "callId",
|
||||
opponentMemberId: "opponentMemberId",
|
||||
bandwidth: { upload: 426, download: 0 },
|
||||
bitrate: {
|
||||
upload: 426,
|
||||
download: 0,
|
||||
audio: {
|
||||
upload: 124,
|
||||
download: 0,
|
||||
},
|
||||
video: {
|
||||
upload: 302,
|
||||
download: 0,
|
||||
},
|
||||
},
|
||||
packetLoss: {
|
||||
total: 0,
|
||||
download: 0,
|
||||
upload: 0,
|
||||
},
|
||||
framerate: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", 0],
|
||||
["LOCAL_VIDEO_TRACK_ID", 30],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 0],
|
||||
["REMOTE_VIDEO_TRACK_ID", 60],
|
||||
]),
|
||||
},
|
||||
resolution: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
|
||||
]),
|
||||
},
|
||||
jitter: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 2],
|
||||
["REMOTE_VIDEO_TRACK_ID", 50],
|
||||
]),
|
||||
codec: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", "opus"],
|
||||
["LOCAL_VIDEO_TRACK_ID", "v8"],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", "opus"],
|
||||
["REMOTE_VIDEO_TRACK_ID", "v9"],
|
||||
]),
|
||||
},
|
||||
transport: [
|
||||
{
|
||||
ip: "ff11::5fa:abcd:999c:c5c5:50000",
|
||||
type: "udp",
|
||||
localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
|
||||
isFocus: true,
|
||||
localCandidateType: "host",
|
||||
remoteCandidateType: "host",
|
||||
networkType: "ethernet",
|
||||
rtt: NaN,
|
||||
},
|
||||
{
|
||||
ip: "10.10.10.2:22222",
|
||||
type: "tcp",
|
||||
localIp: "10.10.10.100:33333",
|
||||
isFocus: true,
|
||||
localCandidateType: "srfx",
|
||||
remoteCandidateType: "srfx",
|
||||
networkType: "ethernet",
|
||||
rtt: null,
|
||||
},
|
||||
],
|
||||
audioConcealment: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", noConcealment],
|
||||
["REMOTE_VIDEO_TRACK_ID", noConcealment],
|
||||
]),
|
||||
totalAudioConcealment: noConcealment,
|
||||
},
|
||||
};
|
||||
|
||||
describe("on flattenObjectRecursive", () => {
|
||||
it("should flatter an Map object", () => {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report.resolution,
|
||||
flatObject,
|
||||
"matrix.call.stats.connection.resolution.",
|
||||
0
|
||||
);
|
||||
expect(flatObject).toEqual({
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
|
||||
-1,
|
||||
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
|
||||
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
|
||||
-1,
|
||||
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
|
||||
});
|
||||
});
|
||||
it("should flatter an Array object", () => {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report.transport,
|
||||
flatObject,
|
||||
"matrix.call.stats.connection.transport.",
|
||||
0
|
||||
);
|
||||
expect(flatObject).toEqual({
|
||||
"matrix.call.stats.connection.transport.0.ip":
|
||||
"ff11::5fa:abcd:999c:c5c5:50000",
|
||||
"matrix.call.stats.connection.transport.0.type": "udp",
|
||||
"matrix.call.stats.connection.transport.0.localIp":
|
||||
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
|
||||
"matrix.call.stats.connection.transport.0.isFocus": true,
|
||||
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
|
||||
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
|
||||
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.0.rtt": "NaN",
|
||||
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
|
||||
"matrix.call.stats.connection.transport.1.type": "tcp",
|
||||
"matrix.call.stats.connection.transport.1.localIp":
|
||||
"10.10.10.100:33333",
|
||||
"matrix.call.stats.connection.transport.1.isFocus": true,
|
||||
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.1.rtt": "null",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("on flattenReportObject Connection Stats", () => {
|
||||
it("should flatten a Report to otel Attributes Object", () => {
|
||||
expect(
|
||||
ObjectFlattener.flattenReportObject(
|
||||
"matrix.call.stats.connection",
|
||||
statsReport.report
|
||||
)
|
||||
).toEqual({
|
||||
"matrix.call.stats.connection.callId": "callId",
|
||||
"matrix.call.stats.connection.opponentMemberId": "opponentMemberId",
|
||||
"matrix.call.stats.connection.bandwidth.download": 0,
|
||||
"matrix.call.stats.connection.bandwidth.upload": 426,
|
||||
"matrix.call.stats.connection.bitrate.audio.download": 0,
|
||||
"matrix.call.stats.connection.bitrate.audio.upload": 124,
|
||||
"matrix.call.stats.connection.bitrate.download": 0,
|
||||
"matrix.call.stats.connection.bitrate.upload": 426,
|
||||
"matrix.call.stats.connection.bitrate.video.download": 0,
|
||||
"matrix.call.stats.connection.bitrate.video.upload": 302,
|
||||
"matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus",
|
||||
"matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8",
|
||||
"matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID":
|
||||
"opus",
|
||||
"matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9",
|
||||
"matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0,
|
||||
"matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30,
|
||||
"matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0,
|
||||
"matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60,
|
||||
"matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2,
|
||||
"matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50,
|
||||
"matrix.call.stats.connection.packetLoss.download": 0,
|
||||
"matrix.call.stats.connection.packetLoss.total": 0,
|
||||
"matrix.call.stats.connection.packetLoss.upload": 0,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
|
||||
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
|
||||
-1,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
|
||||
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
|
||||
"matrix.call.stats.connection.transport.0.ip":
|
||||
"ff11::5fa:abcd:999c:c5c5:50000",
|
||||
"matrix.call.stats.connection.transport.0.type": "udp",
|
||||
"matrix.call.stats.connection.transport.0.localIp":
|
||||
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
|
||||
"matrix.call.stats.connection.transport.0.isFocus": true,
|
||||
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
|
||||
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
|
||||
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.0.rtt": "NaN",
|
||||
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
|
||||
"matrix.call.stats.connection.transport.1.type": "tcp",
|
||||
"matrix.call.stats.connection.transport.1.localIp":
|
||||
"10.10.10.100:33333",
|
||||
"matrix.call.stats.connection.transport.1.isFocus": true,
|
||||
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.1.rtt": "null",
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0,
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0,
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0,
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0,
|
||||
"matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0,
|
||||
"matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("on flattenByteSendStatsReportObject", () => {
|
||||
const byteSentStatsReport = new Map<
|
||||
string,
|
||||
number
|
||||
>() as ByteSentStatsReport;
|
||||
byteSentStatsReport.callId = "callId";
|
||||
byteSentStatsReport.opponentMemberId = "opponentMemberId";
|
||||
byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093);
|
||||
byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448);
|
||||
|
||||
it("should flatten a Report to otel Attributes Object", () => {
|
||||
expect(
|
||||
ObjectFlattener.flattenReportObject(
|
||||
"matrix.call.stats.bytesSend",
|
||||
byteSentStatsReport
|
||||
)
|
||||
).toEqual({
|
||||
"matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093,
|
||||
"matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448,
|
||||
});
|
||||
expect(byteSentStatsReport.callId).toEqual("callId");
|
||||
expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId");
|
||||
});
|
||||
});
|
||||
});
|
||||
171
test/room/checkForParallelCalls-test.ts
Normal file
171
test/room/checkForParallelCalls-test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
||||
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
||||
|
||||
const withFakeTimers = (continuation: () => void) => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
continuation();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
};
|
||||
|
||||
const withMockedPosthog = (
|
||||
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
||||
) => {
|
||||
const posthog = mocked({
|
||||
trackEvent: jest.fn(),
|
||||
} as unknown as PosthogAnalytics);
|
||||
const instanceSpy = jest
|
||||
.spyOn(PosthogAnalytics, "instance", "get")
|
||||
.mockReturnValue(posthog);
|
||||
try {
|
||||
continuation(posthog);
|
||||
} finally {
|
||||
instanceSpy.mockRestore();
|
||||
}
|
||||
};
|
||||
|
||||
const mockRoomState = (
|
||||
groupCallMemberContents: Record<string, unknown>[]
|
||||
): RoomState => {
|
||||
const stateEvents = groupCallMemberContents.map((content) => ({
|
||||
getContent: () => content,
|
||||
}));
|
||||
return { getStateEvents: () => stateEvents } as unknown as RoomState;
|
||||
};
|
||||
|
||||
test("checkForParallelCalls does nothing if all participants are in the same call", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": null, // invalid
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
null, // invalid
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() - 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).toHaveBeenCalledWith({
|
||||
eventName: "ParallelCalls",
|
||||
participantsPerCall: {
|
||||
"1": 2,
|
||||
"2": 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
283
test/video-grid/model-test.ts
Normal file
283
test/video-grid/model-test.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
appendItems,
|
||||
column,
|
||||
cycleTileSize,
|
||||
fillGaps,
|
||||
forEachCellInArea,
|
||||
Grid,
|
||||
row,
|
||||
} from "../../src/video-grid/model";
|
||||
import { TileDescriptor } from "../../src/video-grid/VideoGrid";
|
||||
|
||||
/**
|
||||
* Builds a grid from a string specifying the contents of each cell as a letter.
|
||||
*/
|
||||
function mkGrid(spec: string): Grid {
|
||||
const secondNewline = spec.indexOf("\n", 1);
|
||||
const columns = secondNewline === -1 ? spec.length : secondNewline - 1;
|
||||
const cells = spec.match(/[a-z ]/g) ?? ([] as string[]);
|
||||
const areas = new Set(cells);
|
||||
areas.delete(" "); // Space represents an empty cell, not an area
|
||||
const grid: Grid = { columns, cells: new Array(cells.length) };
|
||||
|
||||
for (const area of areas) {
|
||||
const start = cells.indexOf(area);
|
||||
const end = cells.lastIndexOf(area);
|
||||
const rows = row(end, grid) - row(start, grid) + 1;
|
||||
const columns = column(end, grid) - column(start, grid) + 1;
|
||||
|
||||
forEachCellInArea(start, end, grid, (_c, i) => {
|
||||
grid.cells[i] = {
|
||||
item: { id: area } as unknown as TileDescriptor<unknown>,
|
||||
origin: i === start,
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a grid into a string showing the contents of each cell as a letter.
|
||||
*/
|
||||
function showGrid(g: Grid): string {
|
||||
let result = "\n";
|
||||
g.cells.forEach((c, i) => {
|
||||
if (i > 0 && i % g.columns == 0) result += "\n";
|
||||
result += c?.item.id ?? " ";
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function testFillGaps(title: string, input: string, output: string): void {
|
||||
test(`fillGaps ${title}`, () => {
|
||||
expect(showGrid(fillGaps(mkGrid(input)))).toBe(output);
|
||||
});
|
||||
}
|
||||
|
||||
testFillGaps(
|
||||
"does nothing on an empty grid",
|
||||
`
|
||||
`,
|
||||
`
|
||||
`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"does nothing if there are no gaps",
|
||||
`
|
||||
ab
|
||||
cd
|
||||
ef`,
|
||||
`
|
||||
ab
|
||||
cd
|
||||
ef`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"fills a gap",
|
||||
`
|
||||
a b
|
||||
cde
|
||||
f`,
|
||||
`
|
||||
cab
|
||||
fde`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"fills multiple gaps",
|
||||
`
|
||||
a bc
|
||||
defgh
|
||||
ijkl
|
||||
mno`,
|
||||
`
|
||||
aebch
|
||||
difgl
|
||||
monjk`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"fills a big gap",
|
||||
`
|
||||
abcd
|
||||
e f
|
||||
g h
|
||||
ijkl`,
|
||||
`
|
||||
abcd
|
||||
elhf
|
||||
gkji`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"only moves 1×1 tiles",
|
||||
`
|
||||
|
||||
aa
|
||||
bc`,
|
||||
`
|
||||
bc
|
||||
aa`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"prefers moving around large tiles",
|
||||
`
|
||||
a bc
|
||||
ddde
|
||||
dddf
|
||||
ghij
|
||||
k`,
|
||||
`
|
||||
abce
|
||||
dddf
|
||||
dddj
|
||||
kghi`
|
||||
);
|
||||
|
||||
testFillGaps(
|
||||
"moves through large tiles if necessary",
|
||||
`
|
||||
a bc
|
||||
dddd
|
||||
efgh
|
||||
i`,
|
||||
`
|
||||
afbc
|
||||
dddd
|
||||
iegh`
|
||||
);
|
||||
|
||||
function testCycleTileSize(
|
||||
title: string,
|
||||
tileId: string,
|
||||
input: string,
|
||||
output: string
|
||||
): void {
|
||||
test(`cycleTileSize ${title}`, () => {
|
||||
expect(showGrid(cycleTileSize(tileId, mkGrid(input)))).toBe(output);
|
||||
});
|
||||
}
|
||||
|
||||
testCycleTileSize(
|
||||
"does nothing if the tile is not present",
|
||||
"z",
|
||||
`
|
||||
abcd
|
||||
efgh`,
|
||||
`
|
||||
abcd
|
||||
efgh`
|
||||
);
|
||||
|
||||
testCycleTileSize(
|
||||
"expands a tile to 2×2 in a 3 column layout",
|
||||
"c",
|
||||
`
|
||||
abc
|
||||
def
|
||||
ghi`,
|
||||
`
|
||||
acc
|
||||
bcc
|
||||
def
|
||||
ghi`
|
||||
);
|
||||
|
||||
testCycleTileSize(
|
||||
"expands a tile to 3×3 in a 4 column layout",
|
||||
"g",
|
||||
`
|
||||
abcd
|
||||
efgh`,
|
||||
`
|
||||
abcd
|
||||
eggg
|
||||
fggg
|
||||
h`
|
||||
);
|
||||
|
||||
testCycleTileSize(
|
||||
"restores a tile to 1×1",
|
||||
"b",
|
||||
`
|
||||
abbc
|
||||
dbbe
|
||||
fghi
|
||||
jk`,
|
||||
`
|
||||
abhc
|
||||
djge
|
||||
fik`
|
||||
);
|
||||
|
||||
testCycleTileSize(
|
||||
"expands a tile even in a crowded grid",
|
||||
"c",
|
||||
`
|
||||
abb
|
||||
cbb
|
||||
dde
|
||||
ddf
|
||||
ghi
|
||||
klm`,
|
||||
`
|
||||
abb
|
||||
gbb
|
||||
dde
|
||||
ddf
|
||||
cci
|
||||
cch
|
||||
klm`
|
||||
);
|
||||
|
||||
testCycleTileSize(
|
||||
"does nothing if the tile has no room to expand",
|
||||
"c",
|
||||
`
|
||||
abb
|
||||
cbb
|
||||
dde
|
||||
ddf`,
|
||||
`
|
||||
abb
|
||||
cbb
|
||||
dde
|
||||
ddf`
|
||||
);
|
||||
|
||||
test("appendItems appends 1×1 tiles", () => {
|
||||
const grid1 = `
|
||||
aab
|
||||
aac
|
||||
d`;
|
||||
const grid2 = `
|
||||
aab
|
||||
aac
|
||||
def`;
|
||||
const newItems = ["e", "f"].map(
|
||||
(i) => ({ id: i } as unknown as TileDescriptor<unknown>)
|
||||
);
|
||||
expect(showGrid(appendItems(newItems, mkGrid(grid1)))).toBe(grid2);
|
||||
});
|
||||
Reference in New Issue
Block a user