Skip to content

Commit f621bb8

Browse files
committed
✨ border color to identify dev/test/prd #241
1 parent 6470637 commit f621bb8

File tree

7 files changed

+355
-0
lines changed

7 files changed

+355
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<template>
2+
<Teleport to="body">
3+
<div
4+
v-if="runModeStyle"
5+
:style="runModeStyle"
6+
:title="runModeTitle"
7+
@click="hideTemporarily"
8+
/>
9+
</Teleport>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
type PropsRunModeType = string | RunModeType | (() => MayPromise<string | RunModeType>);
14+
15+
const props = defineProps<{
16+
runMode?: PropsRunModeType;
17+
barSide?: 'top' | 'bottom' | 'left' | 'right';
18+
barSize?: number;
19+
barHide?: number;
20+
apiUri?: string;
21+
}>();
22+
23+
const runModeStyle = shallowRef<Record<string, string> | null>(null);
24+
const runModeTitle = shallowRef('');
25+
26+
async function init() {
27+
let runModeProduce = props.runMode;
28+
if (runModeProduce == null) {
29+
const prefix = useRuntimeConfig().public.apiRoute;
30+
const apiUri = prefix + (props.apiUri ?? '/test/envs/run-mode.json');
31+
runModeProduce = () => $fetch<string>(apiUri, { method: 'post', responseType: 'text' });
32+
}
33+
34+
let runMode: string | null;
35+
if (typeof runModeProduce === 'function') {
36+
try {
37+
runMode = await runModeProduce();
38+
}
39+
catch (err) {
40+
logger.error('failed to load runmode api, use build mode by env', err);
41+
runMode = envRunMode;
42+
}
43+
}
44+
else {
45+
runMode = runModeProduce;
46+
}
47+
48+
runMode = guessRunMode(runMode);
49+
50+
let bgColor = '';
51+
if (runMode === RunMode.Develop) {
52+
bgColor = '#3b82f6'; // bg-blue-500
53+
}
54+
else if (runMode === RunMode.Test) {
55+
bgColor = '#22c55e'; // bg-green-500
56+
}
57+
else if (runMode === RunMode.Local) {
58+
bgColor = '#f97316'; // bg-orange-500
59+
}
60+
61+
if (!bgColor) return;
62+
runModeTitle.value = `run-mode is ${runMode}`;
63+
64+
const baseStyle = {
65+
position: 'fixed',
66+
zIndex: '99999',
67+
backgroundColor: bgColor,
68+
pointerEvents: 'auto',
69+
display: 'block',
70+
};
71+
72+
const size = (props.barSize ?? '2') + 'px';
73+
switch (props.barSide) {
74+
case 'bottom':
75+
Object.assign(baseStyle, { height: size, width: '100vw', bottom: '0', left: '0' });
76+
break;
77+
case 'left':
78+
Object.assign(baseStyle, { width: size, height: '100vh', top: '0', left: '0' });
79+
break;
80+
case 'right':
81+
Object.assign(baseStyle, { width: size, height: '100vh', top: '0', right: '0' });
82+
break;
83+
case 'top':
84+
default:
85+
Object.assign(baseStyle, { height: size, width: '100vw', top: '0', left: '0' });
86+
break;
87+
}
88+
89+
runModeStyle.value = baseStyle;
90+
}
91+
92+
function hideTemporarily() {
93+
const baseStyle = runModeStyle.value;
94+
const hidden = Math.max(props.barHide ?? 10, 0);
95+
runModeStyle.value = { ...baseStyle, display: 'none' };
96+
97+
try {
98+
alert(`hide ${hidden}s, ${runModeTitle.value}`);
99+
}
100+
catch (err) {
101+
logger.error('failed to show alert', err);
102+
}
103+
//
104+
setTimeout(() => runModeStyle.value = baseStyle, hidden * 1000);
105+
}
106+
107+
onMounted(init);
108+
</script>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { mount } from '@vue/test-utils';
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
import RunModeBar from '../components/RunModeBar.vue';
4+
5+
vi.setConfig({ testTimeout: 10000 });
6+
7+
vi.useFakeTimers();
8+
9+
const mockFetch = vi.fn();
10+
vi.stubGlobal('$fetch', mockFetch);
11+
12+
describe('RunModeBar.vue', () => {
13+
beforeEach(() => {
14+
mockFetch.mockReset();
15+
});
16+
17+
it('renders correctly for Develop', async () => {
18+
const wrapper = mount(RunModeBar, {
19+
props: { runMode: 'Develop' },
20+
global: { stubs: { Teleport: true } },
21+
});
22+
await vi.runAllTimersAsync();
23+
expect(wrapper.html()).toContain('#3b82f6');
24+
});
25+
26+
it('renders correctly for Test', async () => {
27+
const wrapper = mount(RunModeBar, {
28+
props: { runMode: 'Test' },
29+
global: { stubs: { Teleport: true } },
30+
});
31+
await vi.runAllTimersAsync();
32+
expect(wrapper.html()).toContain('#22c55e');
33+
});
34+
35+
it('renders with custom barSize', async () => {
36+
const wrapper = mount(RunModeBar, {
37+
props: { runMode: 'Test', barSize: 10 },
38+
global: { stubs: { Teleport: true } },
39+
});
40+
await vi.runAllTimersAsync();
41+
expect(wrapper.html()).toContain('10px');
42+
});
43+
44+
it('hides and reappears after custom time', async () => {
45+
const wrapper = mount(RunModeBar, {
46+
props: { runMode: 'Develop', barHide: 1 },
47+
global: { stubs: { Teleport: true } },
48+
});
49+
await vi.runAllTimersAsync();
50+
51+
const div = wrapper.find('div');
52+
expect(div.exists()).toBe(true);
53+
await div.trigger('click');
54+
expect((wrapper.vm as SafeAny).runModeStyle.display).toBe('none');
55+
56+
await vi.advanceTimersByTimeAsync(2000);
57+
expect((wrapper.vm as SafeAny).runModeStyle.display).toBe('block');
58+
});
59+
60+
it('uses fallback API when runMode is not provided', async () => {
61+
mockFetch.mockResolvedValueOnce('dev');
62+
const wrapper = mount(RunModeBar, {
63+
props: {},
64+
global: { stubs: { Teleport: true } },
65+
});
66+
await vi.runAllTimersAsync();
67+
expect(mockFetch).toHaveBeenCalled();
68+
expect(wrapper.html()).toContain('#3b82f6');
69+
});
70+
71+
it('accepts async function as runMode', async () => {
72+
const wrapper = mount(RunModeBar, {
73+
props: { runMode: async () => 'Test' },
74+
global: { stubs: { Teleport: true } },
75+
});
76+
await vi.runAllTimersAsync();
77+
expect(wrapper.html()).toContain('#22c55e');
78+
});
79+
80+
it('renders nothing when unknown runMode is given', async () => {
81+
const wrapper = mount(RunModeBar, {
82+
props: { runMode: 'Nothing' },
83+
global: { stubs: { Teleport: true } },
84+
});
85+
await vi.runAllTimersAsync();
86+
expect(wrapper.html()).not.toContain('background-color');
87+
});
88+
89+
it('supports left side bar rendering', async () => {
90+
const wrapper = mount(RunModeBar, {
91+
props: { runMode: 'Develop', barSide: 'left', barSize: 5 },
92+
global: { stubs: { Teleport: true } },
93+
});
94+
await vi.runAllTimersAsync();
95+
expect(wrapper.html()).toContain('width: 5px');
96+
});
97+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('guessRunMode', () => {
4+
it('should return null for null or undefined input', () => {
5+
expect(guessRunMode(null)).toBeNull();
6+
expect(guessRunMode(undefined)).toBeNull();
7+
});
8+
9+
it('should return null for empty string', () => {
10+
expect(guessRunMode('')).toBeNull();
11+
});
12+
13+
it('should correctly identify product mode', () => {
14+
expect(guessRunMode('prod')).toBe(RunMode.Product);
15+
expect(guessRunMode('prd')).toBe(RunMode.Product);
16+
expect(guessRunMode('PRODUCTION')).toBe(RunMode.Product);
17+
});
18+
19+
it('should correctly identify test mode', () => {
20+
expect(guessRunMode('test')).toBe(RunMode.Test);
21+
expect(guessRunMode('tst')).toBe(RunMode.Test);
22+
expect(guessRunMode('TESTING')).toBe(RunMode.Test);
23+
});
24+
25+
it('should correctly identify develop mode', () => {
26+
expect(guessRunMode('develop')).toBe(RunMode.Develop);
27+
expect(guessRunMode('dev')).toBe(RunMode.Develop);
28+
expect(guessRunMode('DEVELOPER')).toBe(RunMode.Develop);
29+
});
30+
31+
it('should correctly identify local mode', () => {
32+
expect(guessRunMode('local')).toBe(RunMode.Local);
33+
expect(guessRunMode('lcl')).toBe(RunMode.Local);
34+
expect(guessRunMode('LOCALHOST')).toBe(RunMode.Local);
35+
});
36+
37+
it('should respect index parameter', () => {
38+
// Should not find any match before index 5
39+
expect(guessRunMode('This is a test string', 10)).toBe(RunMode.Test);
40+
41+
// Should find 'dev' starting at index 3
42+
expect(guessRunMode('My development server', 3)).toBe(RunMode.Develop);
43+
});
44+
45+
it('should return null when no match is found', () => {
46+
expect(guessRunMode('randomstring')).toBeNull();
47+
expect(guessRunMode('unknownmode')).toBeNull();
48+
});
49+
});

layers/common/utils/run-mode.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export const RunMode = {
2+
Product: 'Product',
3+
Test: 'Test',
4+
Develop: 'Develop',
5+
Local: 'Local',
6+
Nothing: 'Nothing',
7+
} as const;
8+
9+
export type RunModeKey = keyof typeof RunMode;
10+
export type RunModeType = typeof RunMode[RunModeKey];
11+
12+
/**
13+
* guess runmode by indexing string
14+
*/
15+
export function guessRunMode(str: string | undefined | null, index = 0): RunModeType | null {
16+
if (str) {
17+
const lc = str.toLowerCase();
18+
const mp = {
19+
prod: RunMode.Product,
20+
prd: RunMode.Product,
21+
test: RunMode.Test,
22+
tst: RunMode.Test,
23+
develop: RunMode.Develop,
24+
dev: RunMode.Develop,
25+
local: RunMode.Local,
26+
lcl: RunMode.Local,
27+
};
28+
for (const [key, mode] of Object.entries(mp)) {
29+
if (lc.includes(key, index)) {
30+
return mode;
31+
}
32+
}
33+
}
34+
return null;
35+
}
36+
37+
/*
38+
* https://vite.dev/guide/env-and-mode.html
39+
*/
40+
export const envRunMode = guessRunMode(import.meta.env.MODE) ?? (import.meta.env.PROD ? RunMode.Product : (import.meta.env.DEV ? RunMode.Develop : RunMode.Nothing));
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// server/api/test/envs/run-mode.json.ts
2+
export default defineEventHandler(() => {
3+
const mode = process.env.RUN_MODE ?? process.env.NODE_ENV ?? 'Nothing';
4+
return {
5+
success: true,
6+
data: mode,
7+
};
8+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<div class="m-6 bg-gray-200">
3+
<RunModeBar />
4+
<RunModeBar run-mode="test" bar-side="left" />
5+
<RunModeBar :run-mode="apiRunMode" bar-side="bottom" />
6+
<RunModeBar run-mode="local" bar-side="right" />
7+
<pre class="p-4">
8+
develop-bar at top by default
9+
test-bar at left
10+
dev-bar at bottom by api
11+
local-bar at right
12+
13+
click to hide 10s
14+
</pre>
15+
</div>
16+
</template>
17+
18+
<script lang="ts" setup>
19+
definePageMeta({
20+
name: 'App Run Mode',
21+
});
22+
23+
function apiRunMode() {
24+
return '<R><success>true</success><data>Develop</data></R>';
25+
}
26+
</script>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<AppTab :title="metaName">
3+
<div class="m-6 bg-gray-200">
4+
<RunModeBar />
5+
<RunModeBar run-mode="test" bar-side="left" />
6+
<RunModeBar :run-mode="apiRunMode" bar-side="bottom" />
7+
<RunModeBar run-mode="local" bar-side="right" />
8+
<pre class="p-4">
9+
develop-bar at top by default
10+
test-bar at left
11+
dev-bar at bottom by api
12+
local-bar at right
13+
14+
click to hide 10s
15+
</pre>
16+
</div>
17+
</AppTab>
18+
</template>
19+
20+
<script lang="ts" setup>
21+
const metaName = 'App Run Mode';
22+
definePageMeta({ name: metaName });
23+
24+
function apiRunMode() {
25+
return '<R><success>true</success><data>Develop</data></R>';
26+
}
27+
</script>

0 commit comments

Comments
 (0)