ci: Use Cypress' skip to skip cached links instead of trying to modify the subjects array (which won't work after the tests are registered and running)
parent
b3cfd0d52e
commit
ad92a57ef7
|
|
@ -7,6 +7,7 @@ describe('Article', () => {
|
||||||
.filter((s) => s.trim() !== '')
|
.filter((s) => s.trim() !== '')
|
||||||
: [];
|
: [];
|
||||||
let validationStrategy = null;
|
let validationStrategy = null;
|
||||||
|
let shouldSkipAllTests = false; // Flag to skip tests when all files are cached
|
||||||
|
|
||||||
// Always use HEAD for downloads to avoid timeouts
|
// Always use HEAD for downloads to avoid timeouts
|
||||||
const useHeadForDownloads = true;
|
const useHeadForDownloads = true;
|
||||||
|
|
@ -15,97 +16,6 @@ describe('Article', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
// Initialize the broken links report
|
// Initialize the broken links report
|
||||||
cy.task('initializeBrokenLinksReport');
|
cy.task('initializeBrokenLinksReport');
|
||||||
|
|
||||||
// Get source file paths for incremental validation
|
|
||||||
const testSubjectsData = Cypress.env('test_subjects_data');
|
|
||||||
let sourceFilePaths = subjects; // fallback to subjects if no data available
|
|
||||||
|
|
||||||
if (testSubjectsData) {
|
|
||||||
try {
|
|
||||||
const urlToSourceData = JSON.parse(testSubjectsData);
|
|
||||||
// Extract source file paths from the structured data
|
|
||||||
sourceFilePaths = urlToSourceData.map((item) => item.source);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(
|
|
||||||
'Could not parse test_subjects_data, using subjects as fallback'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only run incremental validation if we have source file paths
|
|
||||||
if (sourceFilePaths.length > 0) {
|
|
||||||
cy.log('🔄 Running incremental validation analysis...');
|
|
||||||
|
|
||||||
// Run incremental validation with proper error handling
|
|
||||||
cy.task('runIncrementalValidation', sourceFilePaths)
|
|
||||||
.then((results) => {
|
|
||||||
validationStrategy = results.validationStrategy;
|
|
||||||
|
|
||||||
// Save cache statistics and validation strategy for reporting
|
|
||||||
cy.task('saveCacheStatistics', results.cacheStats);
|
|
||||||
cy.task('saveValidationStrategy', validationStrategy);
|
|
||||||
|
|
||||||
// Update subjects to only test files that need validation
|
|
||||||
if (results.filesToValidate.length > 0) {
|
|
||||||
// Convert file paths to URLs using shared utility via Cypress task
|
|
||||||
const urlPromises = results.filesToValidate.map((file) =>
|
|
||||||
cy.task('filePathToUrl', file.filePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.wrap(Promise.all(urlPromises)).then((urls) => {
|
|
||||||
subjects = urls;
|
|
||||||
|
|
||||||
cy.log(
|
|
||||||
`📊 Cache Analysis: ${results.cacheStats.hitRate}% hit rate`
|
|
||||||
);
|
|
||||||
cy.log(
|
|
||||||
`🔄 Testing ${subjects.length} pages (${results.cacheStats.cacheHits} cached)`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// All files are cached, no validation needed
|
|
||||||
subjects = [];
|
|
||||||
cy.log('✨ All files cached - skipping validation');
|
|
||||||
cy.log(
|
|
||||||
`📊 Cache hit rate: ${results.cacheStats.hitRate}% (${results.cacheStats.cacheHits}/${results.cacheStats.totalFiles} files cached)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
cy.log('❌ Incremental validation failed: ' + error.message);
|
|
||||||
cy.log(
|
|
||||||
'🔄 Falling back to test all provided subjects without cache optimization'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set fallback mode but don't fail the test
|
|
||||||
validationStrategy = {
|
|
||||||
fallback: true,
|
|
||||||
error: error.message,
|
|
||||||
unchanged: [],
|
|
||||||
changed: sourceFilePaths.map((filePath) => ({
|
|
||||||
filePath,
|
|
||||||
error: 'fallback',
|
|
||||||
})),
|
|
||||||
total: sourceFilePaths.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.log(`📋 Testing ${subjects.length} pages in fallback mode`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cy.log('⚠️ No source file paths available, using all provided subjects');
|
|
||||||
|
|
||||||
// Set a simple validation strategy when no source data is available
|
|
||||||
validationStrategy = {
|
|
||||||
noSourceData: true,
|
|
||||||
unchanged: [],
|
|
||||||
changed: [],
|
|
||||||
total: subjects.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.log(
|
|
||||||
`📋 Testing ${subjects.length} pages without incremental validation`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to identify download links
|
// Helper function to identify download links
|
||||||
|
|
@ -216,64 +126,103 @@ describe('Article', () => {
|
||||||
// Test implementation for subjects
|
// Test implementation for subjects
|
||||||
// Add debugging information about test subjects
|
// Add debugging information about test subjects
|
||||||
it('Test Setup Validation', function () {
|
it('Test Setup Validation', function () {
|
||||||
cy.log(`📋 Test Configuration:`);
|
cy.log(`📋 Initial Test Configuration:`);
|
||||||
cy.log(` • Test subjects count: ${subjects.length}`);
|
cy.log(` • Initial test subjects count: ${subjects.length}`);
|
||||||
cy.log(` • Validation strategy: ${validationStrategy || 'Not set'}`);
|
|
||||||
|
|
||||||
// Check if we're in fallback mode due to cache system issues
|
// Get source file paths for incremental validation
|
||||||
if (validationStrategy && validationStrategy.fallback) {
|
const testSubjectsData = Cypress.env('test_subjects_data');
|
||||||
cy.log('⚠️ Running in fallback mode due to cache system error');
|
let sourceFilePaths = subjects; // fallback to subjects if no data available
|
||||||
cy.log(` • Error: ${validationStrategy.error}`);
|
|
||||||
cy.log(' • All files will be tested without cache optimization');
|
|
||||||
|
|
||||||
// In fallback mode, if we have no subjects, that might be expected
|
if (testSubjectsData) {
|
||||||
if (subjects.length === 0) {
|
try {
|
||||||
cy.log('ℹ️ No subjects to test in fallback mode');
|
const urlToSourceData = JSON.parse(testSubjectsData);
|
||||||
|
// Extract source file paths from the structured data
|
||||||
|
sourceFilePaths = urlToSourceData.map((item) => item.source);
|
||||||
|
cy.log(` • Source files to analyze: ${sourceFilePaths.length}`);
|
||||||
|
} catch (e) {
|
||||||
cy.log(
|
cy.log(
|
||||||
' This indicates no test subjects were provided to the runner'
|
'⚠️ Could not parse test_subjects_data, using subjects as fallback'
|
||||||
);
|
);
|
||||||
} else {
|
sourceFilePaths = subjects;
|
||||||
cy.log(`✅ Testing ${subjects.length} subjects in fallback mode`);
|
|
||||||
}
|
}
|
||||||
} else if (subjects.length === 0) {
|
}
|
||||||
cy.log('ℹ️ No test subjects to validate - analyzing reason:');
|
|
||||||
|
|
||||||
// Check if this is due to cache optimization
|
// Only run incremental validation if we have source file paths
|
||||||
const testSubjectsData = Cypress.env('test_subjects_data');
|
if (sourceFilePaths.length > 0) {
|
||||||
if (
|
cy.log('🔄 Running incremental validation analysis...');
|
||||||
testSubjectsData &&
|
cy.log(
|
||||||
testSubjectsData !== '[]' &&
|
` • Analyzing ${sourceFilePaths.length} files: ${sourceFilePaths.join(', ')}`
|
||||||
testSubjectsData !== ''
|
);
|
||||||
) {
|
|
||||||
cy.log('✅ Cache optimization is active - all files were cached');
|
// Run incremental validation with proper error handling
|
||||||
try {
|
cy.task('runIncrementalValidation', sourceFilePaths).then((results) => {
|
||||||
const urlToSourceData = JSON.parse(testSubjectsData);
|
if (!results) {
|
||||||
cy.log(`📊 Files processed: ${urlToSourceData.length}`);
|
cy.log('⚠️ No results returned from incremental validation');
|
||||||
cy.log(
|
cy.log(
|
||||||
'💡 This means all links have been validated recently and are cached'
|
'🔄 Falling back to test all provided subjects without cache optimization'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if results have expected structure
|
||||||
|
if (!results.validationStrategy || !results.cacheStats) {
|
||||||
|
cy.log('⚠️ Incremental validation results missing expected fields');
|
||||||
|
cy.log(` • Results: ${JSON.stringify(results)}`);
|
||||||
|
cy.log(
|
||||||
|
'🔄 Falling back to test all provided subjects without cache optimization'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationStrategy = results.validationStrategy;
|
||||||
|
|
||||||
|
// Save cache statistics and validation strategy for reporting
|
||||||
|
cy.task('saveCacheStatistics', results.cacheStats);
|
||||||
|
cy.task('saveValidationStrategy', validationStrategy);
|
||||||
|
|
||||||
|
// Update subjects to only test files that need validation
|
||||||
|
if (results.filesToValidate && results.filesToValidate.length > 0) {
|
||||||
|
// Convert file paths to URLs using shared utility via Cypress task
|
||||||
|
const urlPromises = results.filesToValidate.map((file) =>
|
||||||
|
cy.task('filePathToUrl', file.filePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.wrap(Promise.all(urlPromises)).then((urls) => {
|
||||||
|
subjects = urls;
|
||||||
|
|
||||||
|
cy.log(
|
||||||
|
`📊 Cache Analysis: ${results.cacheStats.hitRate}% hit rate`
|
||||||
|
);
|
||||||
|
cy.log(
|
||||||
|
`🔄 Testing ${subjects.length} pages (${results.cacheStats.cacheHits} cached)`
|
||||||
|
);
|
||||||
|
cy.log('✅ Incremental validation completed - ready to test');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// All files are cached, no validation needed
|
||||||
|
shouldSkipAllTests = true; // Set flag to skip all tests
|
||||||
|
cy.log('✨ All files cached - will skip all validation tests');
|
||||||
|
cy.log(
|
||||||
|
`📊 Cache hit rate: ${results.cacheStats.hitRate}% (${results.cacheStats.cacheHits}/${results.cacheStats.totalFiles} files cached)`
|
||||||
);
|
);
|
||||||
cy.log('🎯 No new validation needed - this is the expected outcome');
|
cy.log('🎯 No new validation needed - this is the expected outcome');
|
||||||
} catch (e) {
|
cy.log('⏭️ All link validation tests will be skipped');
|
||||||
cy.log(
|
|
||||||
'✅ Cache optimization active (could not parse detailed data)'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
cy.log('⚠️ No test subjects data available');
|
|
||||||
cy.log(' Possible reasons:');
|
|
||||||
cy.log(' • No files were provided to test');
|
|
||||||
cy.log(' • File mapping failed during setup');
|
|
||||||
cy.log(' • No files matched the test criteria');
|
|
||||||
cy.log(
|
|
||||||
' This is not necessarily an error - may be expected for some runs'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cy.log(`✅ Ready to test ${subjects.length} pages`);
|
cy.log('⚠️ No source file paths available, using all provided subjects');
|
||||||
subjects.slice(0, 5).forEach((subject) => cy.log(` • ${subject}`));
|
|
||||||
if (subjects.length > 5) {
|
// Set a simple validation strategy when no source data is available
|
||||||
cy.log(` ... and ${subjects.length - 5} more pages`);
|
validationStrategy = {
|
||||||
}
|
noSourceData: true,
|
||||||
|
unchanged: [],
|
||||||
|
changed: [],
|
||||||
|
total: subjects.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.log(
|
||||||
|
`📋 Testing ${subjects.length} pages without incremental validation`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for truly problematic scenarios
|
// Check for truly problematic scenarios
|
||||||
|
|
@ -303,67 +252,55 @@ describe('Article', () => {
|
||||||
|
|
||||||
subjects.forEach((subject) => {
|
subjects.forEach((subject) => {
|
||||||
it(`${subject} has valid internal links`, function () {
|
it(`${subject} has valid internal links`, function () {
|
||||||
|
// Skip test if all files are cached
|
||||||
|
if (shouldSkipAllTests) {
|
||||||
|
cy.log('✅ All files cached - skipping internal links test');
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add error handling for page visit failures
|
// Add error handling for page visit failures
|
||||||
cy.visit(`${subject}`, { timeout: 20000 })
|
cy.visit(`${subject}`, { timeout: 20000 }).then(() => {
|
||||||
.then(() => {
|
cy.log(`✅ Successfully loaded page: ${subject}`);
|
||||||
cy.log(`✅ Successfully loaded page: ${subject}`);
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
cy.log(`❌ Failed to load page: ${subject}`);
|
|
||||||
cy.log(` • Error: ${error.message}`);
|
|
||||||
cy.log('💡 This could indicate:');
|
|
||||||
cy.log(' • Hugo server not running or crashed');
|
|
||||||
cy.log(' • Invalid URL or routing issue');
|
|
||||||
cy.log(' • Network connectivity problems');
|
|
||||||
throw error; // Re-throw to fail the test properly
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test internal links
|
// Test internal links
|
||||||
cy.get('article, .api-content')
|
cy.get('article, .api-content').then(($article) => {
|
||||||
.then(($article) => {
|
// Find links without failing the test if none are found
|
||||||
// Find links without failing the test if none are found
|
const $links = $article.find('a[href^="/"]');
|
||||||
const $links = $article.find('a[href^="/"]');
|
if ($links.length === 0) {
|
||||||
if ($links.length === 0) {
|
cy.log('No internal links found on this page');
|
||||||
cy.log('No internal links found on this page');
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
cy.log(`🔍 Testing ${$links.length} internal links on ${subject}`);
|
||||||
|
|
||||||
|
// Now test each link
|
||||||
|
cy.wrap($links).each(($a) => {
|
||||||
|
const href = $a.attr('href');
|
||||||
|
const linkText = $a.text().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
testLink(href, linkText, subject);
|
||||||
|
} catch (error) {
|
||||||
|
cy.log(`❌ Error testing link ${href}: ${error.message}`);
|
||||||
|
throw error; // Re-throw to fail the test
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.log(`🔍 Testing ${$links.length} internal links on ${subject}`);
|
|
||||||
|
|
||||||
// Now test each link
|
|
||||||
cy.wrap($links).each(($a) => {
|
|
||||||
const href = $a.attr('href');
|
|
||||||
const linkText = $a.text().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
testLink(href, linkText, subject);
|
|
||||||
} catch (error) {
|
|
||||||
cy.log(`❌ Error testing link ${href}: ${error.message}`);
|
|
||||||
throw error; // Re-throw to fail the test
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
cy.log(`❌ Error finding article content on ${subject}`);
|
|
||||||
cy.log(` • Error: ${error.message}`);
|
|
||||||
cy.log('💡 This could indicate:');
|
|
||||||
cy.log(' • Page structure changed (missing article/.api-content)');
|
|
||||||
cy.log(' • Page failed to render properly');
|
|
||||||
cy.log(' • JavaScript errors preventing DOM updates');
|
|
||||||
throw error;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`${subject} has valid anchor links`, function () {
|
it(`${subject} has valid anchor links`, function () {
|
||||||
cy.visit(`${subject}`)
|
// Skip test if all files are cached
|
||||||
.then(() => {
|
if (shouldSkipAllTests) {
|
||||||
cy.log(`✅ Successfully loaded page for anchor testing: ${subject}`);
|
cy.log('✅ All files cached - skipping anchor links test');
|
||||||
})
|
this.skip();
|
||||||
.catch((error) => {
|
return;
|
||||||
cy.log(`❌ Failed to load page for anchor testing: ${subject}`);
|
}
|
||||||
cy.log(` • Error: ${error.message}`);
|
|
||||||
throw error;
|
cy.visit(`${subject}`).then(() => {
|
||||||
});
|
cy.log(`✅ Successfully loaded page for anchor testing: ${subject}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Define selectors for anchor links to ignore, such as behavior triggers
|
// Define selectors for anchor links to ignore, such as behavior triggers
|
||||||
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
|
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
|
||||||
|
|
@ -414,6 +351,13 @@ describe('Article', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`${subject} has valid external links`, function () {
|
it(`${subject} has valid external links`, function () {
|
||||||
|
// Skip test if all files are cached
|
||||||
|
if (shouldSkipAllTests) {
|
||||||
|
cy.log('✅ All files cached - skipping external links test');
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should skip external links entirely
|
// Check if we should skip external links entirely
|
||||||
if (Cypress.env('skipExternalLinks') === true) {
|
if (Cypress.env('skipExternalLinks') === true) {
|
||||||
cy.log(
|
cy.log(
|
||||||
|
|
@ -422,82 +366,62 @@ describe('Article', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.visit(`${subject}`)
|
cy.visit(`${subject}`).then(() => {
|
||||||
.then(() => {
|
cy.log(
|
||||||
cy.log(
|
`✅ Successfully loaded page for external link testing: ${subject}`
|
||||||
`✅ Successfully loaded page for external link testing: ${subject}`
|
);
|
||||||
);
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
cy.log(
|
|
||||||
`❌ Failed to load page for external link testing: ${subject}`
|
|
||||||
);
|
|
||||||
cy.log(` • Error: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define allowed external domains to test
|
// Define allowed external domains to test
|
||||||
const allowedExternalDomains = ['github.com', 'kapa.ai'];
|
const allowedExternalDomains = ['github.com', 'kapa.ai'];
|
||||||
|
|
||||||
// Test external links
|
// Test external links
|
||||||
cy.get('article, .api-content')
|
cy.get('article, .api-content').then(($article) => {
|
||||||
.then(($article) => {
|
// Find links without failing the test if none are found
|
||||||
// Find links without failing the test if none are found
|
const $links = $article.find('a[href^="http"]');
|
||||||
const $links = $article.find('a[href^="http"]');
|
if ($links.length === 0) {
|
||||||
if ($links.length === 0) {
|
cy.log('No external links found on this page');
|
||||||
cy.log('No external links found on this page');
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cy.log(
|
cy.log(`🔍 Found ${$links.length} total external links on ${subject}`);
|
||||||
`🔍 Found ${$links.length} total external links on ${subject}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter links to only include allowed domains
|
// Filter links to only include allowed domains
|
||||||
const $allowedLinks = $links.filter((_, el) => {
|
const $allowedLinks = $links.filter((_, el) => {
|
||||||
const href = el.getAttribute('href');
|
const href = el.getAttribute('href');
|
||||||
try {
|
try {
|
||||||
const url = new URL(href);
|
const url = new URL(href);
|
||||||
return allowedExternalDomains.some(
|
return allowedExternalDomains.some(
|
||||||
(domain) =>
|
(domain) =>
|
||||||
url.hostname === domain || url.hostname.endsWith(`.${domain}`)
|
url.hostname === domain || url.hostname.endsWith(`.${domain}`)
|
||||||
);
|
|
||||||
} catch (urlError) {
|
|
||||||
cy.log(`⚠️ Invalid URL found: ${href}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($allowedLinks.length === 0) {
|
|
||||||
cy.log('No links to allowed external domains found on this page');
|
|
||||||
cy.log(
|
|
||||||
` • Allowed domains: ${allowedExternalDomains.join(', ')}`
|
|
||||||
);
|
);
|
||||||
return;
|
} catch (urlError) {
|
||||||
|
cy.log(`⚠️ Invalid URL found: ${href}`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.log(
|
|
||||||
`🌐 Testing ${$allowedLinks.length} links to allowed external domains`
|
|
||||||
);
|
|
||||||
cy.wrap($allowedLinks).each(($a) => {
|
|
||||||
const href = $a.attr('href');
|
|
||||||
const linkText = $a.text().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
testLink(href, linkText, subject);
|
|
||||||
} catch (error) {
|
|
||||||
cy.log(
|
|
||||||
`❌ Error testing external link ${href}: ${error.message}`
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
cy.log(`❌ Error processing external links on ${subject}`);
|
|
||||||
cy.log(` • Error: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($allowedLinks.length === 0) {
|
||||||
|
cy.log('No links to allowed external domains found on this page');
|
||||||
|
cy.log(` • Allowed domains: ${allowedExternalDomains.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.log(
|
||||||
|
`🌐 Testing ${$allowedLinks.length} links to allowed external domains`
|
||||||
|
);
|
||||||
|
cy.wrap($allowedLinks).each(($a) => {
|
||||||
|
const href = $a.attr('href');
|
||||||
|
const linkText = $a.text().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
testLink(href, linkText, subject);
|
||||||
|
} catch (error) {
|
||||||
|
cy.log(`❌ Error testing external link ${href}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue