diff --git a/app/src/main/java/com/httrack/android/OptionsActivity.java b/app/src/main/java/com/httrack/android/OptionsActivity.java
index 5e42ae8e..2d90aeec 100755
--- a/app/src/main/java/com/httrack/android/OptionsActivity.java
+++ b/app/src/main/java/com/httrack/android/OptionsActivity.java
@@ -207,7 +207,8 @@ public static class BrowserId extends Tab {
@Fields({ R.id.checkAcceptCookies, R.id.editCookiesFile,
R.id.radioCheckDocumentType, R.id.checkParseJavaFiles, R.id.radioSpider,
R.id.checkSitemap, R.id.editSitemapUrl, R.id.checkUpdateHacks,
- R.id.checkUrlHacks, R.id.checkTolerentRequests, R.id.checkForceHttp10 })
+ R.id.checkUrlHacks, R.id.editHostAlias, R.id.checkTolerentRequests,
+ R.id.checkForceHttp10 })
public static class Spider extends Tab {
}
@@ -246,8 +247,7 @@ public static class MimeDefs extends Tab {
@HelpPage("guide.html#droid/opt-experts-only")
@Fields({ R.id.checkUseCacheForUpdates, R.id.radioPrimaryScanRule,
R.id.textTravelMode, R.id.radioTravelMode, R.id.radioGlobalTravelMode,
- R.id.radioRewriteLinks, R.id.editStripQuery, R.id.editHostAlias,
- R.id.checkActivateDebugging })
+ R.id.radioRewriteLinks, R.id.editStripQuery, R.id.checkActivateDebugging })
public static class ExpertsOnly extends Tab {
}
diff --git a/app/src/main/res/layout/activity_options_expertsonly.xml b/app/src/main/res/layout/activity_options_expertsonly.xml
index 990322c2..3b684967 100644
--- a/app/src/main/res/layout/activity_options_expertsonly.xml
+++ b/app/src/main/res/layout/activity_options_expertsonly.xml
@@ -205,19 +205,6 @@
android:inputType="textNoSuggestions" />
-
-
-
-
+
+
+
+
layouts() {
- final Set files = new HashSet();
- for (final File dir : resDir().listFiles()) {
- if (!dir.isDirectory() || !dir.getName().startsWith("layout")) {
- continue;
- }
- for (final File file : dir.listFiles()) {
- if (file.getName().endsWith(".xml")) {
- files.add(file);
- }
- }
- }
- return files;
- }
-
private static String option(final String id) {
for (final String[] field : NUMERIC_FIELDS) {
if (field[0].equals(id)) {
@@ -82,7 +53,7 @@ public void numericFieldsOfferADecimalPointOnlyWhereTheEngineTakesOne()
factory.setNamespaceAware(true);
final DocumentBuilder builder = factory.newDocumentBuilder();
final Set seen = new HashSet();
- for (final File layout : layouts()) {
+ for (final File layout : TestSources.layouts()) {
final Document doc = builder.parse(layout);
final NodeList nodes = doc.getElementsByTagName("*");
for (int i = 0; i < nodes.getLength(); i++) {
diff --git a/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java b/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java
new file mode 100644
index 00000000..55b391cc
--- /dev/null
+++ b/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java
@@ -0,0 +1,107 @@
+package com.httrack.android;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+
+/**
+ * A tab's declared field must be in that tab's own layout: leaving the tab
+ * looks it up, and WidgetDataExchange throws on a view it cannot find.
+ */
+public class OptionsTabFieldsTest {
+ /** @ActivityId + @Fields of one tab, as declared in the source. */
+ private static final Pattern TAB = Pattern.compile(
+ "@ActivityId\\(R\\.layout\\.(\\w+)\\)(.*?)public static class",
+ Pattern.DOTALL);
+
+ private static final Pattern FIELD_ID = Pattern.compile("R\\.id\\.(\\w+)");
+
+ private static final Pattern LAYOUT_ID = Pattern
+ .compile("android:id=\"@\\+?id/(\\w+)\"");
+
+ private static List ids(final Pattern pattern, final String text) {
+ final List ids = new ArrayList();
+ final Matcher m = pattern.matcher(text);
+ while (m.find()) {
+ ids.add(m.group(1));
+ }
+ return ids;
+ }
+
+ /** Layout of each option tab, with the field ids that tab declares. */
+ private static Map> tabs() throws IOException {
+ final Map> tabs = new LinkedHashMap>();
+ final Matcher tab = TAB.matcher(TestSources.javaSource("OptionsActivity"));
+ while (tab.find()) {
+ final List fields = ids(FIELD_ID, tab.group(2));
+ // An annotation order this pattern cannot read would yield none.
+ assertFalse("no field parsed for " + tab.group(1), fields.isEmpty());
+ assertFalse("twice: " + tab.group(1), tabs.containsKey(tab.group(1)));
+ tabs.put(tab.group(1), fields);
+ }
+ assertTrue("parsed " + tabs.size() + " tabs", tabs.size() > 10);
+ return tabs;
+ }
+
+ @Test
+ public void everyTabFieldLivesInThatTabsLayout() throws IOException {
+ for (final Map.Entry> tab : tabs().entrySet()) {
+ final List files = TestSources.layouts(tab.getKey());
+ assertFalse("no layout named " + tab.getKey(), files.isEmpty());
+ for (final File file : files) {
+ final Set declared = new HashSet(ids(LAYOUT_ID,
+ TestSources.read(file)));
+ for (final String field : tab.getValue()) {
+ assertTrue(file.getName() + " has no " + field,
+ declared.contains(field));
+ }
+ }
+ }
+ }
+
+ /* The other direction: a view the mapper knows but no tab claims is never
+ saved, and nothing at runtime says so. */
+ @Test
+ public void everyMappedOptionViewIsClaimedByATab() throws IOException {
+ final Set mapped = new HashSet(ids(FIELD_ID,
+ TestSources.javaSource("OptionsMapper")));
+ assertTrue("parsed " + mapped.size() + " mapped ids", mapped.size() > 80);
+ final Map> tabs = tabs();
+ int claimed = 0;
+ for (final File file : TestSources.layouts()) {
+ if (!file.getName().startsWith("activity_options_")) {
+ continue;
+ }
+ final String layout = file.getName().replaceFirst("\\.xml$", "");
+ for (final String id : ids(LAYOUT_ID, TestSources.read(file))) {
+ if (!mapped.contains(id)) {
+ continue;
+ }
+ final List owners = new ArrayList();
+ for (final Map.Entry> tab : tabs.entrySet()) {
+ if (tab.getValue().contains(id)) {
+ owners.add(tab.getKey());
+ }
+ }
+ assertEquals(id + " in " + layout + " claimed by " + owners, 1,
+ owners.size());
+ assertEquals(id + " is claimed by " + owners.get(0), layout,
+ owners.get(0));
+ claimed++;
+ }
+ }
+ assertTrue("checked " + claimed + " views", claimed > 50);
+ }
+}
diff --git a/app/src/test/java/com/httrack/android/TestSources.java b/app/src/test/java/com/httrack/android/TestSources.java
new file mode 100644
index 00000000..8b49e69b
--- /dev/null
+++ b/app/src/test/java/com/httrack/android/TestSources.java
@@ -0,0 +1,58 @@
+package com.httrack.android;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checked-in sources the tests read; they run with the app project as
+ * working directory. */
+final class TestSources {
+ private TestSources() {
+ }
+
+ private static File dir(final String path) {
+ for (final String prefix : new String[] { "", "app/" }) {
+ final File dir = new File(prefix + path);
+ if (dir.isDirectory()) {
+ return dir;
+ }
+ }
+ throw new IllegalStateException("no " + path + " below "
+ + new File(".").getAbsolutePath());
+ }
+
+ /** Every layout, qualified variants such as layout-land/ included. */
+ static List layouts() {
+ return layouts(null);
+ }
+
+ /** Every variant of NAME, all of which may be inflated; every layout when
+ * NAME is null. */
+ static List layouts(final String name) {
+ final List files = new ArrayList();
+ for (final File res : dir("src/main/res").listFiles()) {
+ if (!res.isDirectory() || !res.getName().startsWith("layout")) {
+ continue;
+ }
+ for (final File file : res.listFiles()) {
+ if (name == null ? file.getName().endsWith(".xml")
+ : file.getName().equals(name + ".xml")) {
+ files.add(file);
+ }
+ }
+ }
+ return files;
+ }
+
+ static String read(final File file) throws IOException {
+ return new String(Files.readAllBytes(file.toPath()), "UTF-8");
+ }
+
+ /** Source of the com.httrack.android class NAME. */
+ static String javaSource(final String name) throws IOException {
+ return read(new File(dir("src/main/java"), "com/httrack/android/" + name
+ + ".java"));
+ }
+}