Files
splatoon2.ink/test/social/posts.test.js

223 lines
8.1 KiB
JavaScript

import { test, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import SchedulePost from '../../src/app/social/posts/SchedulePost.js';
import SalmonRunPost from '../../src/app/social/posts/SalmonRunPost.js';
import NewStagePost from '../../src/app/social/posts/NewStagePost.js';
import SplatfestPost from '../../src/app/social/posts/SplatfestPost.js';
import { createPosts } from '../../src/app/social/posts/index.js';
import { storage, nextRun, fakeClient, json, seed } from './support.js';
const NOW = 3600 * 1000; // an hour boundary, as getTopOfCurrentHour returns
const stage = (id, name) => ({ id, name, image: `/images/stage/${id}.png` });
const rotation = (start, a, b, rule) => ({
start_time: start,
end_time: start + 7200,
stage_a: a,
stage_b: b,
rule: { key: rule, name: rule },
});
let s, client;
beforeEach(async () => {
mock.timers.enable({ apis: ['Date'], now: NOW * 1000 });
s = storage();
client = fakeClient('bluesky');
await seed(s.publicBucket, 'data/festivals.json', {
na: { festivals: [], results: [] },
eu: { festivals: [], results: [] },
jp: { festivals: [], results: [] },
});
});
afterEach(() => mock.timers.reset());
function pinTime(post, time) {
if (post instanceof SchedulePost)
mock.timers.setTime(time * 1000);
else
post.getDataTime = async () => time;
post.getImage = async () => new Uint8Array([1]);
return post;
}
test('createPosts builds every post against the same storage and clients', () => {
const screenshots = {};
const posts = createPosts(s, [client], screenshots);
assert.deepEqual(
posts.map(t => t.getKey()),
['schedule', 'gear', 'salmonrun', 'weapon', 'newstage', 'splatfest-na', 'splatfest-eu', 'splatfest-jp'],
);
assert.ok(
posts.every(
t => t.publicStorage === s.publicStorage && t.clients[0] === client && t.screenshots === screenshots,
),
);
});
test('schedule: posts the rotation for the current hour, with new-stage wording when the stage is new', async () => {
const reef = stage('0', 'The Reef'),
skipper = stage('22', 'Skipper Pavilion');
await seed(s.publicBucket, 'data/schedules.json', {
regular: [rotation(NOW, reef, skipper, 'Turf War')],
gachi: [rotation(NOW, reef, skipper, 'Splat Zones')],
league: [rotation(NOW, reef, skipper, 'Rainmaker')],
});
await seed(s.privateBucket, 'stages.json', [
{ ...reef, first_seen: -1, first_available: -1 },
{ ...skipper, first_seen: NOW - 7200, first_available: NOW },
]);
const post = pinTime(new SchedulePost(s, [client]), NOW);
await post.maybePost();
assert.equal(client.sent[0].status, 'NEW STAGE: Skipper Pavilion is now open! #maprotation #splatoon2');
assert.ok(await s.publicBucket.get('twitter-images/schedule.png'));
const later = pinTime(new SchedulePost(nextRun(s), [client]), NOW + 3600);
assert.equal(await later.maybePost(), false); // the active rotation was already posted
});
test('schedule: the plain rotation text names the ranked and league rules', async () => {
const reef = stage('0', 'The Reef'),
fitness = stage('1', 'Musselforge Fitness');
await seed(s.publicBucket, 'data/schedules.json', {
regular: [rotation(NOW, reef, fitness, 'Turf War')],
gachi: [rotation(NOW, reef, fitness, 'Splat Zones')],
league: [rotation(NOW, reef, fitness, 'Rainmaker')],
});
const post = pinTime(new SchedulePost(s, [client]), NOW);
assert.equal(
await post.getText(await post.getData()),
'Splatoon 2 map rotation: Ranked game mode: Splat Zones, League game mode: Rainmaker #maprotation',
);
});
test('salmon run: posts when a shift opens, remembers it, and posts again when it closes', async () => {
const shift = {
start_time: NOW,
end_time: NOW + 3600 * 36,
stage: { name: 'Spawning Grounds' },
weapons: [{ id: '0', weapon: { name: 'Splattershot' } }],
};
await seed(s.publicBucket, 'data/coop-schedules.json', { schedules: [shift], details: [shift] });
await seed(s.publicBucket, 'data/timeline.json', { coop: { reward_gear: { gear: { name: 'Cap' } } } });
await pinTime(new SalmonRunPost(nextRun(s), [client]), NOW).maybePost();
assert.equal(
client.sent[0].status,
'Salmon Run is now open on Spawning Grounds! Current reward gear is the Cap. #salmonrun #splatoon2',
);
assert.deepEqual(await json(s.privateBucket, 'salmonrun-previousSchedule.json'), shift);
assert.equal(await pinTime(new SalmonRunPost(nextRun(s), [client]), NOW + 3600).maybePost(), false); // mid-shift, not a 12-hour mark
await pinTime(new SalmonRunPost(nextRun(s), [client]), NOW + 3600 * 12).maybePost();
assert.match(client.sent[1].status, /is still open on Spawning Grounds/);
const next = { start_time: NOW + 3600 * 40, end_time: NOW + 3600 * 76 };
await seed(s.publicBucket, 'data/coop-schedules.json', { schedules: [next], details: [] });
await pinTime(new SalmonRunPost(nextRun(s), [client]), NOW + 3600 * 36).maybePost();
assert.equal(
client.sent[2].status,
'Salmon Run is now closed. The next shift starts in 4 hours! #salmonrun #splatoon2',
);
});
test('new stage: posts the mirrored stage image the hour the stage was first seen', async () => {
await seed(s.privateBucket, 'stages.json', [
{
id: '22',
name: 'Skipper Pavilion',
image: '/images/stage/22.png',
first_seen: NOW,
first_available: NOW + 7200,
},
]);
await s.publicBucket.put('assets/splatnet/images/stage/22.png', new Uint8Array([7]));
const post = new NewStagePost(s, [client]);
post.getDataTime = async () => NOW;
await post.maybePost();
assert.equal(
client.sent[0].status,
'NEW STAGE: The first schedules for Skipper Pavilion have been posted! Start playing the new stage when this post is 2 hours old. #splatoon2',
);
assert.deepEqual(client.sent[0].media[0].file, new Uint8Array([7]));
});
test('splatfest: a global fest posts once, from the first region', async () => {
const fest = {
festival_id: 5,
names: { alpha_short: 'A', bravo_short: 'B' },
times: { announce: 0, start: NOW, end: NOW + 3600, result: NOW + 7200 },
};
await seed(s.publicBucket, 'data/festivals.json', {
na: { festivals: [fest], results: [] },
eu: { festivals: [fest], results: [] },
jp: { festivals: [fest], results: [] },
});
const sentBy = [];
for (const region of ['na', 'eu', 'jp']) {
const c = fakeClient('bluesky');
await pinTime(new SplatfestPost(region, s, [c]), NOW).maybePost();
if (c.sent.length)
sentBy.push([region, c.sent[0].status]);
}
assert.deepEqual(sentBy, [['na', 'The global Splatfest is now open! #splatfest #splatoon2']]);
});
test('schedule: catches up in the second hour and checkpoints the rotation start', async () => {
const reef = stage('0', 'The Reef');
const fitness = stage('1', 'Musselforge Fitness');
const rotations = [NOW, NOW + 7200].map(time => rotation(time, reef, fitness, 'Turf War'));
await seed(s.publicBucket, 'data/schedules.json', {
regular: rotations,
gachi: rotations,
league: rotations,
});
const failedClient = fakeClient('bluesky', { fail: true });
const failed = pinTime(new SchedulePost(s, [failedClient]), NOW);
assert.equal((await failed.maybePost()).ok, false);
assert.equal(await json(s.privateBucket, 'bluesky-lastPostTimes.json'), null);
const catchUp = pinTime(new SchedulePost(nextRun(s), [client]), NOW + 3600);
assert.equal((await catchUp.maybePost()).ok, true);
assert.equal((await json(s.privateBucket, 'bluesky-lastPostTimes.json')).schedule, NOW);
assert.ok(await s.publicBucket.get('twitter-images/schedule.png'));
const repeated = pinTime(new SchedulePost(nextRun(s), [client]), NOW + 3600);
assert.equal(await repeated.maybePost(), false);
assert.equal(client.sent.length, 1);
const next = pinTime(new SchedulePost(nextRun(s), [client]), NOW + 7200);
assert.equal((await next.maybePost()).ok, true);
assert.equal((await json(s.privateBucket, 'bluesky-lastPostTimes.json')).schedule, NOW + 7200);
assert.equal(client.sent.length, 2);
const expired = pinTime(new SchedulePost(nextRun(s), [client]), NOW + 14400);
assert.equal(await expired.maybePost(), false);
});