mirror of
https://github.com/misenhower/splatoon3.ink.git
synced 2026-10-24 22:23:04 -05:00
Add developer controls for page state previews
This commit is contained in:
22
src/components/Debug/DevControls.vue
Normal file
22
src/components/Debug/DevControls.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap justify-center items-center gap-2 text-xs">
|
||||
<TimeOffsetSelector />
|
||||
|
||||
<label class="flex items-center gap-2 bg-zinc-700 p-1 rounded-sm">
|
||||
<span class="bg-zinc-500 rounded-sm font-semibold px-1">Page state</span>
|
||||
<select v-model="preview.mode" class="bg-zinc-800">
|
||||
<option value="live">Live data</option>
|
||||
<option value="loading">Loading</option>
|
||||
<option value="empty">Empty</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TimeOffsetSelector from './TimeOffsetSelector.vue';
|
||||
import { usePagePreviewStore } from '@/stores/pagePreview.mjs';
|
||||
|
||||
const preview = usePagePreviewStore();
|
||||
</script>
|
||||
@@ -27,16 +27,33 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import PageSkeleton from './PageSkeleton.vue';
|
||||
import { usePagePreviewStore } from '@/stores/pagePreview.mjs';
|
||||
|
||||
const props = defineProps({
|
||||
sources: { type: Array, required: true },
|
||||
variant: { type: String, required: true },
|
||||
});
|
||||
|
||||
const ready = computed(() => props.sources.every(source => source.data !== null));
|
||||
const failed = computed(() => props.sources.some(source => source.data === null && source.error));
|
||||
const preview = import.meta.env.DEV ? usePagePreviewStore() : null;
|
||||
|
||||
const ready = computed(() => {
|
||||
if (import.meta.env.DEV && ['loading', 'error'].includes(preview.mode)) return false;
|
||||
|
||||
return props.sources.every(source => source.data !== null);
|
||||
});
|
||||
|
||||
const failed = computed(() => {
|
||||
if (import.meta.env.DEV && preview.mode === 'loading') return false;
|
||||
if (import.meta.env.DEV && preview.mode === 'error') return true;
|
||||
|
||||
return props.sources.some(source => source.data === null && source.error);
|
||||
});
|
||||
|
||||
function retry() {
|
||||
if (import.meta.env.DEV && preview.mode === 'error') {
|
||||
preview.mode = 'live';
|
||||
}
|
||||
|
||||
for (const source of props.sources) {
|
||||
if (source.data === null) source.update();
|
||||
}
|
||||
|
||||
@@ -50,20 +50,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TimeOffsetSelector v-if="isDev" class="mb-4" />
|
||||
<DevControls v-if="isDev" class="mb-4" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import NavButtons from '@/components/NavButtons.vue';
|
||||
import LanguageButton from '@/components/LanguageButton.vue';
|
||||
import TimeOffsetSelector from '@/components/Debug/TimeOffsetSelector.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
});
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
const DevControls = import.meta.env.DEV
|
||||
? defineAsyncComponent(() => import('@/components/Debug/DevControls.vue'))
|
||||
: null;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import { getEmptyPreviewData, usePagePreviewStore } from './pagePreview.mjs';
|
||||
|
||||
// Endpoint store definition (used for each individual data endpoint)
|
||||
function defineEndpointStore(id, endpoint, transform = null) {
|
||||
return defineStore(`data/${id}`, () => {
|
||||
const data = shallowRef(null);
|
||||
const loadedData = shallowRef(null);
|
||||
const preview = import.meta.env.DEV ? usePagePreviewStore() : null;
|
||||
const data = import.meta.env.DEV
|
||||
? computed(() => preview.mode === 'empty' ? getEmptyPreviewData(id) : loadedData.value)
|
||||
: loadedData;
|
||||
const isUpdating = ref(false);
|
||||
const error = ref(null);
|
||||
let pending;
|
||||
@@ -50,11 +55,11 @@ function defineEndpointStore(id, endpoint, transform = null) {
|
||||
throw new Error('Missing data payload');
|
||||
}
|
||||
|
||||
data.value = json;
|
||||
loadedData.value = json;
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
return { data, update, setData, isUpdating, error };
|
||||
return { data, loadedData, update, setData, isUpdating, error };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
31
src/stores/pagePreview.mjs
Normal file
31
src/stores/pagePreview.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const usePagePreviewStore = import.meta.env.DEV
|
||||
? defineStore('debug/pagePreview', {
|
||||
state: () => ({ mode: 'live' }),
|
||||
})
|
||||
: null;
|
||||
|
||||
const emptyData = {
|
||||
schedules: {
|
||||
currentFest: null,
|
||||
regularSchedules: { nodes: [] },
|
||||
bankaraSchedules: { nodes: [] },
|
||||
xSchedules: { nodes: [] },
|
||||
festSchedules: { nodes: [] },
|
||||
eventSchedules: { nodes: [] },
|
||||
vsStages: { nodes: [] },
|
||||
coopGroupingSchedule: {
|
||||
regularSchedules: { nodes: [] },
|
||||
bigRunSchedules: { nodes: [] },
|
||||
teamContestSchedules: { nodes: [] },
|
||||
},
|
||||
},
|
||||
gear: { gesotown: { pickupBrand: null, limitedGears: [] } },
|
||||
coop: { coopResult: { monthlyGear: null } },
|
||||
festivals: {},
|
||||
};
|
||||
|
||||
export function getEmptyPreviewData(endpoint) {
|
||||
return emptyData[endpoint];
|
||||
}
|
||||
71
src/stores/pagePreview.test.mjs
Normal file
71
src/stores/pagePreview.test.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useGearDataStore, useSchedulesDataStore } from './data.mjs';
|
||||
import { usePagePreviewStore } from './pagePreview.mjs';
|
||||
import { useGearStore } from './gear.mjs';
|
||||
import {
|
||||
useRegularSchedulesStore,
|
||||
useAnarchySeriesSchedulesStore,
|
||||
useAnarchyOpenSchedulesStore,
|
||||
useXSchedulesStore,
|
||||
useEventSchedulesStore,
|
||||
useSalmonRunSchedulesStore,
|
||||
useEggstraWorkSchedulesStore,
|
||||
} from './schedules.mjs';
|
||||
import { useUSSplatfestsStore } from './splatfests.mjs';
|
||||
|
||||
describe('page state preview', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it('provides valid empty results for every data page before requests complete', () => {
|
||||
usePagePreviewStore().mode = 'empty';
|
||||
|
||||
expect(useGearDataStore().data).not.toBeNull();
|
||||
expect(useSchedulesDataStore().data).not.toBeNull();
|
||||
expect(useGearStore().dailyDropBrand).toBeNull();
|
||||
expect(useGearStore().regularGear).toEqual([]);
|
||||
expect(useUSSplatfestsStore().festivals).toEqual([]);
|
||||
|
||||
const scheduleStores = [
|
||||
useRegularSchedulesStore,
|
||||
useAnarchySeriesSchedulesStore,
|
||||
useAnarchyOpenSchedulesStore,
|
||||
useXSchedulesStore,
|
||||
useEventSchedulesStore,
|
||||
useSalmonRunSchedulesStore,
|
||||
useEggstraWorkSchedulesStore,
|
||||
];
|
||||
|
||||
for (const useStore of scheduleStores) {
|
||||
expect(useStore().currentSchedules).toEqual([]);
|
||||
expect(useStore().activeSchedule).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('holds the empty preview through data updates and restores the latest live data', () => {
|
||||
const preview = usePagePreviewStore();
|
||||
const gear = useGearDataStore();
|
||||
gear.setData({ data: { version: 1 } });
|
||||
|
||||
preview.mode = 'empty';
|
||||
gear.setData({ data: { version: 2 } });
|
||||
expect(gear.data.gesotown.limitedGears).toEqual([]);
|
||||
|
||||
preview.mode = 'live';
|
||||
expect(gear.data).toEqual({ version: 2 });
|
||||
});
|
||||
|
||||
it('restores the pending state when leaving an empty preview without live data', () => {
|
||||
const preview = usePagePreviewStore();
|
||||
const gear = useGearDataStore();
|
||||
|
||||
preview.mode = 'empty';
|
||||
expect(gear.data).not.toBeNull();
|
||||
|
||||
preview.mode = 'live';
|
||||
expect(gear.data).toBeNull();
|
||||
expect(gear.error).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user