@sun-asterisk/sungen 2.6.12 → 2.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +215 -65
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +173 -32
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +84 -84
- package/dist/dashboard/types.d.ts +35 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +24 -3
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +28 -7
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts +15 -0
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +7 -2
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +7 -0
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +20 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/selector-key-resolver.d.ts +55 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -0
- package/dist/exporters/selector-key-resolver.js +208 -0
- package/dist/exporters/selector-key-resolver.js.map +1 -0
- package/dist/exporters/test-data-resolver.d.ts +15 -2
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +61 -8
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +1 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +28 -3
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +34 -6
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +13 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +4 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +48 -14
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +22 -11
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +71 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +23 -8
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +45 -6
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +316 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -13
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +20 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +70 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +23 -8
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +44 -6
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +291 -0
- package/dist/orchestrator/templates/playwright.config.ts +25 -8
- package/dist/orchestrator/templates/specs-base.ts +9 -0
- package/dist/orchestrator/templates/specs-locale-fixture.ts +105 -0
- package/package.json +1 -1
- package/src/cli/commands/delivery.ts +256 -65
- package/src/cli/index.ts +1 -1
- package/src/dashboard/snapshot-builder.ts +207 -32
- package/src/dashboard/templates/index.html +84 -84
- package/src/dashboard/types.ts +40 -3
- package/src/exporters/csv-exporter.ts +36 -7
- package/src/exporters/json-exporter.ts +22 -2
- package/src/exporters/playwright-report-parser.ts +20 -0
- package/src/exporters/selector-key-resolver.ts +190 -0
- package/src/exporters/test-data-resolver.ts +65 -7
- package/src/exporters/types.ts +1 -0
- package/src/exporters/xlsx-exporter.ts +61 -7
- package/src/generators/test-generator/code-generator.ts +14 -0
- package/src/orchestrator/ai-rules-updater.ts +4 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +48 -14
- package/src/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +22 -11
- package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +71 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +23 -8
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +45 -6
- package/src/orchestrator/templates/ai-instructions/claude-config.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +316 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -13
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +4 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +20 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +70 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +23 -8
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +44 -6
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +4 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +291 -0
- package/src/orchestrator/templates/playwright.config.ts +25 -8
- package/src/orchestrator/templates/specs-base.ts +9 -0
- package/src/orchestrator/templates/specs-locale-fixture.ts +105 -0
- package/dist/orchestrator/templates/playwright.config.d.ts +0 -10
- package/dist/orchestrator/templates/playwright.config.d.ts.map +0 -1
- package/dist/orchestrator/templates/playwright.config.js +0 -104
- package/dist/orchestrator/templates/playwright.config.js.map +0 -1
- package/dist/orchestrator/templates/specs-base.d.ts +0 -14
- package/dist/orchestrator/templates/specs-base.d.ts.map +0 -1
- package/dist/orchestrator/templates/specs-base.js +0 -77
- package/dist/orchestrator/templates/specs-base.js.map +0 -1
- package/dist/orchestrator/templates/specs-test-data.d.ts +0 -16
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +0 -1
- package/dist/orchestrator/templates/specs-test-data.js +0 -151
- package/dist/orchestrator/templates/specs-test-data.js.map +0 -1
|
@@ -18,6 +18,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
18
18
|
import { parseFeatureMetadata } from '../exporters/feature-parser';
|
|
19
19
|
import { parseSpecFile } from '../exporters/spec-parser';
|
|
20
20
|
import { loadTestData } from '../exporters/test-data-resolver';
|
|
21
|
+
import { loadSelectorKeyMap } from '../exporters/selector-key-resolver';
|
|
21
22
|
import { loadPlaywrightReport } from '../exporters/playwright-report-parser';
|
|
22
23
|
import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
|
|
23
24
|
import { buildScreenSnapshot } from '../exporters/json-exporter';
|
|
@@ -26,11 +27,20 @@ import { EnvironmentInfo } from '../exporters/types';
|
|
|
26
27
|
import {
|
|
27
28
|
AggregateSummary,
|
|
28
29
|
DashboardSnapshot,
|
|
30
|
+
FeatureSnapshot,
|
|
31
|
+
LocaleSnapshot,
|
|
29
32
|
ScenarioSnapshot,
|
|
30
33
|
ScreenSnapshot,
|
|
34
|
+
ScreenSummaryStats,
|
|
31
35
|
SNAPSHOT_VERSION,
|
|
32
36
|
} from './types';
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Locale code displayed when a screen has no overlay variants — the base
|
|
40
|
+
* test-data file alone. Matches the delivery CLI's `DEFAULT_BASE_LOCALE`.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_BASE_LOCALE = 'vi';
|
|
43
|
+
|
|
34
44
|
export interface DashboardTarget {
|
|
35
45
|
name: string;
|
|
36
46
|
isFlow: boolean;
|
|
@@ -80,12 +90,62 @@ function buildOneScreen(
|
|
|
80
90
|
env: EnvironmentInfo
|
|
81
91
|
): ScreenSnapshot | null {
|
|
82
92
|
const base = qaDir(cwd, target);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
const featuresDir = path.join(base, 'features');
|
|
94
|
+
if (!fs.existsSync(featuresDir)) return null;
|
|
95
|
+
|
|
96
|
+
// Enumerate all .feature files inside the screen/flow — matches delivery's
|
|
97
|
+
// discovery so dashboard scenarios share TC IDs with the exported CSVs.
|
|
98
|
+
const featureBaseNames = fs.readdirSync(featuresDir)
|
|
99
|
+
.filter((f) => f.endsWith('.feature'))
|
|
100
|
+
.map((f) => f.slice(0, -'.feature'.length))
|
|
101
|
+
.sort();
|
|
102
|
+
if (featureBaseNames.length === 0) return null;
|
|
103
|
+
|
|
87
104
|
const specMdFile = path.join(base, 'requirements', 'spec.md');
|
|
88
|
-
const
|
|
105
|
+
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : undefined;
|
|
106
|
+
|
|
107
|
+
const features: FeatureSnapshot[] = [];
|
|
108
|
+
for (const featureBaseName of featureBaseNames) {
|
|
109
|
+
const fs2 = buildOneFeature(cwd, target, featureBaseName, env, specLink);
|
|
110
|
+
if (fs2) features.push(fs2);
|
|
111
|
+
}
|
|
112
|
+
if (features.length === 0) return null;
|
|
113
|
+
|
|
114
|
+
// Flat scenarios across all features (each scenario already carries its
|
|
115
|
+
// own `featureBaseName`, so consumers that want to group can do so cheaply).
|
|
116
|
+
const scenarios = features.flatMap((f) => f.scenarios);
|
|
117
|
+
const label = target.isFlow ? `flow/${target.name}` : target.name;
|
|
118
|
+
const primary = features[0];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: label,
|
|
122
|
+
isFlow: target.isFlow,
|
|
123
|
+
featureName: primary.featureName,
|
|
124
|
+
featurePath: primary.featurePath,
|
|
125
|
+
specLink,
|
|
126
|
+
summary: rollupScreenSummary(features),
|
|
127
|
+
scenarios,
|
|
128
|
+
features,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build a FeatureSnapshot for a single `.feature` file inside a screen/flow.
|
|
134
|
+
* TC IDs are generated using `featureBaseName` as the prefix so they match
|
|
135
|
+
* the delivery CSV (e.g. `HOME-MODAL-UI-001`, not `HOME-UI-001`).
|
|
136
|
+
*/
|
|
137
|
+
function buildOneFeature(
|
|
138
|
+
cwd: string,
|
|
139
|
+
target: DashboardTarget,
|
|
140
|
+
featureBaseName: string,
|
|
141
|
+
env: EnvironmentInfo,
|
|
142
|
+
specLink: string | undefined,
|
|
143
|
+
): FeatureSnapshot | null {
|
|
144
|
+
const base = qaDir(cwd, target);
|
|
145
|
+
const genBase = generatedDir(cwd, target);
|
|
146
|
+
const featureFile = path.join(base, 'features', `${featureBaseName}.feature`);
|
|
147
|
+
const testDataFile = path.join(base, 'test-data', `${featureBaseName}.yaml`);
|
|
148
|
+
const specFile = path.join(genBase, `${featureBaseName}.spec.ts`);
|
|
89
149
|
|
|
90
150
|
if (!fs.existsSync(featureFile)) return null;
|
|
91
151
|
|
|
@@ -93,33 +153,157 @@ function buildOneScreen(
|
|
|
93
153
|
const spec = fs.existsSync(specFile)
|
|
94
154
|
? parseSpecFile(specFile)
|
|
95
155
|
: { tests: [] };
|
|
96
|
-
const testData = fs.existsSync(testDataFile) ? loadTestData(testDataFile) : {};
|
|
97
|
-
const results = resultsFile ? loadPlaywrightReport(resultsFile) : null;
|
|
98
|
-
|
|
99
156
|
const merged = mergeFeatureAndSpec(feature, spec);
|
|
100
|
-
const label = target.isFlow ? `flow/${target.name}` : target.name;
|
|
101
|
-
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : undefined;
|
|
102
157
|
|
|
103
158
|
// Screens declare their URL inline in the .feature ("Path: /awards"). Flows
|
|
104
|
-
// don't
|
|
105
|
-
// the flow's selectors YAML — that's where the entry point is recorded.
|
|
159
|
+
// don't, so fall back to the first `type: 'page'` entry in selectors YAML.
|
|
106
160
|
let featurePath = feature.featurePath;
|
|
161
|
+
const selectorsFile = path.join(base, 'selectors', `${featureBaseName}.yaml`);
|
|
107
162
|
if (!featurePath && target.isFlow) {
|
|
108
|
-
const selectorsFile = path.join(base, 'selectors', `${target.name}.yaml`);
|
|
109
163
|
featurePath = readPagePathFromSelectors(selectorsFile);
|
|
110
164
|
}
|
|
165
|
+
const selectorKeyMap = loadSelectorKeyMap(selectorsFile);
|
|
166
|
+
|
|
167
|
+
// Discover locale variants by scanning <feature>-test-result*.json files.
|
|
168
|
+
// Always includes base ('') as the first entry, even if its results file
|
|
169
|
+
// is missing — UI still needs to render the feature.
|
|
170
|
+
const variants = discoverLocaleVariants(genBase, featureBaseName);
|
|
171
|
+
|
|
172
|
+
// Build per-locale ScreenSnapshot, then collect into FeatureSnapshot.locales[].
|
|
173
|
+
// Base variant is also used to populate the top-level (`scenarios`, `summary`)
|
|
174
|
+
// for back-compat with consumers that don't read the locales array.
|
|
175
|
+
const locales: LocaleSnapshot[] = [];
|
|
176
|
+
let baseBuilt: { scenarios: ScenarioSnapshot[]; summary: ScreenSummaryStats; featureName: string; featurePath?: string; specLink?: string } | null = null;
|
|
177
|
+
|
|
178
|
+
for (const variant of variants) {
|
|
179
|
+
const testData = fs.existsSync(testDataFile)
|
|
180
|
+
? loadTestData(testDataFile, variant.locale || null)
|
|
181
|
+
: {};
|
|
182
|
+
const results = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
183
|
+
|
|
184
|
+
const built = buildScreenSnapshot({
|
|
185
|
+
screen: featureBaseName,
|
|
186
|
+
isFlow: target.isFlow,
|
|
187
|
+
featureName: feature.featureName,
|
|
188
|
+
featurePath,
|
|
189
|
+
specLink,
|
|
190
|
+
merged,
|
|
191
|
+
testData,
|
|
192
|
+
results,
|
|
193
|
+
env,
|
|
194
|
+
featureBaseName,
|
|
195
|
+
selectorKeyMap,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
locales.push({
|
|
199
|
+
locale: variant.locale,
|
|
200
|
+
displayCode: variant.displayCode,
|
|
201
|
+
summary: built.summary,
|
|
202
|
+
scenarios: built.scenarios,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (variant.locale === '') {
|
|
206
|
+
baseBuilt = built;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
111
209
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
210
|
+
// Guarantee a base entry — every feature in the dashboard must have
|
|
211
|
+
// something to fall back to even if base results file is missing.
|
|
212
|
+
if (!baseBuilt && locales.length > 0) {
|
|
213
|
+
baseBuilt = {
|
|
214
|
+
scenarios: locales[0].scenarios,
|
|
215
|
+
summary: locales[0].summary,
|
|
216
|
+
featureName: feature.featureName,
|
|
217
|
+
featurePath,
|
|
218
|
+
specLink,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!baseBuilt) return null;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
featureBaseName,
|
|
225
|
+
featureName: baseBuilt.featureName,
|
|
226
|
+
featurePath: baseBuilt.featurePath,
|
|
227
|
+
specLink: baseBuilt.specLink,
|
|
228
|
+
summary: baseBuilt.summary,
|
|
229
|
+
scenarios: baseBuilt.scenarios,
|
|
230
|
+
// Only expose `locales` array when 2+ variants exist — single-locale
|
|
231
|
+
// features stay slimmer and UI can skip the locale tab UI entirely.
|
|
232
|
+
locales: locales.length >= 2 ? locales : undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A single locale variant of a feature.
|
|
238
|
+
* `locale === ''` denotes the base (file `<name>-test-result.json`,
|
|
239
|
+
* test-data without overlay merge). Variants come from
|
|
240
|
+
* `<name>-test-result.<locale>.json` filenames.
|
|
241
|
+
*/
|
|
242
|
+
interface FeatureLocaleVariant {
|
|
243
|
+
locale: string;
|
|
244
|
+
displayCode: string;
|
|
245
|
+
resultsPath: string | null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Scan the generated directory for `<basename>-test-result*.json` files
|
|
250
|
+
* and infer locale codes. The base entry is always returned first; locale
|
|
251
|
+
* variants follow alphabetically so sheet/tab order stays deterministic.
|
|
252
|
+
*
|
|
253
|
+
* Mirrors `discoverLocaleVariants` in `src/cli/commands/delivery.ts` so the
|
|
254
|
+
* dashboard and the CSV/XLSX delivery agree on what locales exist for a
|
|
255
|
+
* given feature.
|
|
256
|
+
*/
|
|
257
|
+
function discoverLocaleVariants(genDir: string, featureBaseName: string): FeatureLocaleVariant[] {
|
|
258
|
+
const prefix = `${featureBaseName}-test-result`;
|
|
259
|
+
const variants: FeatureLocaleVariant[] = [];
|
|
260
|
+
|
|
261
|
+
const basePath = path.join(genDir, `${prefix}.json`);
|
|
262
|
+
variants.push({
|
|
263
|
+
locale: '',
|
|
264
|
+
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
265
|
+
resultsPath: fs.existsSync(basePath) ? basePath : null,
|
|
122
266
|
});
|
|
267
|
+
|
|
268
|
+
if (fs.existsSync(genDir)) {
|
|
269
|
+
const localeFiles = fs.readdirSync(genDir)
|
|
270
|
+
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
271
|
+
.sort();
|
|
272
|
+
for (const f of localeFiles) {
|
|
273
|
+
const locale = f.slice(prefix.length + 1, -'.json'.length);
|
|
274
|
+
if (!locale) continue;
|
|
275
|
+
variants.push({
|
|
276
|
+
locale,
|
|
277
|
+
displayCode: locale.toUpperCase(),
|
|
278
|
+
resultsPath: path.join(genDir, f),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return variants;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Combine per-feature summaries into a single screen-level rollup.
|
|
288
|
+
* passRate is recomputed across the combined passed+failed totals so a
|
|
289
|
+
* screen with one all-passing feature and one all-failing feature shows
|
|
290
|
+
* 50% rather than the arithmetic mean of two rates.
|
|
291
|
+
*/
|
|
292
|
+
function rollupScreenSummary(features: FeatureSnapshot[]): ScreenSummaryStats {
|
|
293
|
+
const out: ScreenSummaryStats = {
|
|
294
|
+
total: 0, passed: 0, failed: 0, pending: 0, na: 0, notCompiled: 0, passRate: 0,
|
|
295
|
+
};
|
|
296
|
+
for (const f of features) {
|
|
297
|
+
out.total += f.summary.total;
|
|
298
|
+
out.passed += f.summary.passed;
|
|
299
|
+
out.failed += f.summary.failed;
|
|
300
|
+
out.pending += f.summary.pending;
|
|
301
|
+
out.na += f.summary.na;
|
|
302
|
+
out.notCompiled += f.summary.notCompiled;
|
|
303
|
+
}
|
|
304
|
+
const executed = out.passed + out.failed;
|
|
305
|
+
out.passRate = executed > 0 ? out.passed / executed : 0;
|
|
306
|
+
return out;
|
|
123
307
|
}
|
|
124
308
|
|
|
125
309
|
// ----------------------------------------------------------------------------
|
|
@@ -178,15 +362,6 @@ function generatedDir(cwd: string, target: DashboardTarget): string {
|
|
|
178
362
|
: path.join(cwd, 'specs', 'generated', target.name);
|
|
179
363
|
}
|
|
180
364
|
|
|
181
|
-
function resolveResultsPath(cwd: string, target: DashboardTarget): string | null {
|
|
182
|
-
const genDir = generatedDir(cwd, target);
|
|
183
|
-
const perTarget = path.join(genDir, `${target.name}-test-result.json`);
|
|
184
|
-
if (fs.existsSync(perTarget)) return perTarget;
|
|
185
|
-
const global = path.join(cwd, 'test-results', 'results.json');
|
|
186
|
-
if (fs.existsSync(global)) return global;
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
365
|
// ----------------------------------------------------------------------------
|
|
191
366
|
// Discovery (also used by CLI)
|
|
192
367
|
// ----------------------------------------------------------------------------
|